Skip to content

Commit 44ff485

Browse files
authored
feat!: Add per-execution runId, at-most-once tracking, and cross-process tracker resumption (#249)
1 parent 92f799f commit 44ff485

10 files changed

Lines changed: 1288 additions & 155 deletions

File tree

pkgs/sdk/server-ai/src/Config/ConfigFactory.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ private LdAiCompletionConfig BuildCompletionFromDefault(
8686
key,
8787
defaultValue.Enabled ?? true,
8888
variationKey: "",
89-
version: 0,
89+
version: 1,
9090
messages,
9191
defaultValue.Model,
9292
defaultValue.Provider,
@@ -124,18 +124,26 @@ private IReadOnlyList<Message> InterpolateMessages(
124124

125125
private Func<LdAiConfigBase, ILdAiConfigTracker> TrackerFactoryFor(Context context)
126126
{
127-
return cfg => new LdAiConfigTracker(_client, cfg, context);
127+
return cfg => new LdAiConfigTracker(
128+
_client,
129+
Guid.NewGuid().ToString(),
130+
cfg.Key,
131+
cfg.VariationKey,
132+
cfg.Version,
133+
context,
134+
cfg.Model?.Name,
135+
cfg.Provider?.Name);
128136
}
129137

130138
private static (bool Enabled, string VariationKey, int Version, string Mode) ParseMeta(LdValue value)
131139
{
132140
var meta = value.Get("_ldMeta");
133141
var enabled = meta.Get("enabled").AsBool;
134142
var variationKey = meta.Get("variationKey").AsString ?? "";
135-
var version = meta.Get("version").AsInt;
143+
var versionValue = meta.Get("version");
144+
var version = versionValue.IsNull ? 1 : versionValue.AsInt;
136145
// Default to the completion mode when _ldMeta.mode is missing or non-string: legacy
137-
// flags predate the mode tag, so treating them as completion configs matches the
138-
// existing shape on the wire.
146+
// flags predate the mode tag and were always served as completion configs.
139147
var mode = meta.Get("mode").AsString ?? LdAiCompletionConfig.Mode;
140148
return (enabled, variationKey, version, mode);
141149
}

pkgs/sdk/server-ai/src/Config/ModelConfig.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections.Generic;
2+
using System.Collections.Immutable;
23

34
namespace LaunchDarkly.Sdk.Server.Ai.Config;
45

@@ -25,7 +26,11 @@ public sealed record ModelConfig
2526
internal ModelConfig(string name, IReadOnlyDictionary<string, LdValue> parameters, IReadOnlyDictionary<string, LdValue> custom)
2627
{
2728
Name = name;
28-
Parameters = parameters;
29-
Custom = custom;
29+
// Materialize into an ImmutableDictionary so a consumer that downcasts to
30+
// IDictionary<> can't mutate the stored map. The cast still succeeds (the
31+
// contract is still IReadOnlyDictionary<>) but write members throw
32+
// NotSupportedException at runtime, matching the typed read-only promise.
33+
Parameters = parameters?.ToImmutableDictionary() ?? ImmutableDictionary<string, LdValue>.Empty;
34+
Custom = custom?.ToImmutableDictionary() ?? ImmutableDictionary<string, LdValue>.Empty;
3035
}
3136
}

pkgs/sdk/server-ai/src/Interfaces/ILdAiClient.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,17 @@ public LdAiCompletionConfig CompletionConfig(string key, Context context, LdAiCo
4141
[Obsolete("Use CompletionConfig instead.")]
4242
public LdAiCompletionConfig Config(string key, Context context, LdAiCompletionConfigDefault defaultValue = null,
4343
IReadOnlyDictionary<string, object> variables = null);
44+
45+
/// <summary>
46+
/// Reconstructs a tracker from a resumption token. This enables cross-process scenarios
47+
/// such as deferred feedback, where a tracker's runId needs to be reused in a different
48+
/// process or at a later time.
49+
///
50+
/// The reconstructed tracker will have empty model and provider names, as these are not
51+
/// included in the resumption token.
52+
/// </summary>
53+
/// <param name="resumptionToken">the resumption token obtained from <see cref="ILdAiConfigTracker.ResumptionToken"/></param>
54+
/// <param name="context">the context to use for track events</param>
55+
/// <returns>a tracker associated with the original runId</returns>
56+
public ILdAiConfigTracker CreateTracker(string resumptionToken, Context context);
4457
}

pkgs/sdk/server-ai/src/Interfaces/ILdAiConfigTracker.cs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,36 @@
55
namespace LaunchDarkly.Sdk.Server.Ai.Interfaces;
66

77
/// <summary>
8-
/// A utility capable of generating events related to a specific AI model
9-
/// configuration.
8+
/// Records metrics for a single AI run.
109
/// </summary>
10+
/// <remarks>
11+
/// All events emitted by a tracker share a runId (a UUIDv4) so LaunchDarkly can correlate
12+
/// them. See individual track methods for their specific semantics.
13+
/// Call <c>CreateTracker</c> on the AI Config to start a new run. A
14+
/// <see cref="ResumptionToken"/> preserves the runId, so events emitted by a tracker
15+
/// reconstructed in another process correlate with the original tracker's runId.
16+
/// </remarks>
1117
public interface ILdAiConfigTracker
1218
{
19+
/// <summary>
20+
/// A URL-safe Base64-encoded token that can be used to reconstruct this tracker in a different
21+
/// process or at a later time. The token contains the runId, configKey, variationKey, and version.
22+
///
23+
/// Use <c>LdAiConfigTracker.FromResumptionToken</c> to reconstruct a tracker from this token.
24+
/// </summary>
25+
public string ResumptionToken { get; }
26+
27+
/// <summary>
28+
/// A summary of the metrics tracked by this tracker.
29+
/// </summary>
30+
public MetricSummary Summary { get; }
31+
1332
/// <summary>
1433
/// Tracks a duration metric related to this config. For example, if a particular operation
1534
/// related to usage of the AI model takes 100ms, this can be tracked and made available in
1635
/// LaunchDarkly.
1736
/// </summary>
37+
/// <remarks>Records at most once per Tracker; further calls are ignored.</remarks>
1838
/// <param name="durationMs">the duration in milliseconds</param>
1939
public void TrackDuration(float durationMs);
2040

@@ -30,28 +50,38 @@ public interface ILdAiConfigTracker
3050
/// <typeparam name="T">type of the task's result</typeparam>
3151
/// <returns>the task</returns>
3252
public Task<T> TrackDurationOfTask<T>(Task<T> task);
33-
53+
3454
/// <summary>
3555
/// Tracks the time it takes for the first token to be generated.
3656
/// </summary>
57+
/// <remarks>Records at most once per Tracker; further calls are ignored.</remarks>
3758
/// <param name="timeToFirstTokenMs">the duration in milliseconds</param>
3859
public void TrackTimeToFirstToken(float timeToFirstTokenMs);
3960

4061
/// <summary>
4162
/// Tracks feedback (positive or negative) related to the output of the model.
4263
/// </summary>
64+
/// <remarks>Records at most once per Tracker; further calls are ignored.</remarks>
4365
/// <param name="feedback">the feedback</param>
4466
/// <exception cref="ArgumentOutOfRangeException">thrown if the feedback value is not <see cref="Feedback.Positive"/> or <see cref="Feedback.Negative"/></exception>
4567
public void TrackFeedback(Feedback feedback);
4668

4769
/// <summary>
4870
/// Tracks a generation event related to this config.
4971
/// </summary>
72+
/// <remarks>
73+
/// Records at most once per Tracker. TrackSuccess and TrackError share state; only
74+
/// one of the two can record per Tracker, and subsequent calls are ignored.
75+
/// </remarks>
5076
public void TrackSuccess();
5177

5278
/// <summary>
5379
/// Tracks an unsuccessful generation event related to this config.
5480
/// </summary>
81+
/// <remarks>
82+
/// Records at most once per Tracker. TrackSuccess and TrackError share state; only
83+
/// one of the two can record per Tracker, and subsequent calls are ignored.
84+
/// </remarks>
5585
public void TrackError();
5686

5787
/// <summary>
@@ -86,13 +116,18 @@ public interface ILdAiConfigTracker
86116
/// Task is automatically measured and recorded as the latency metric associated with this request.
87117
///
88118
/// </summary>
119+
/// <remarks>
120+
/// Subsequent calls re-run the task but emit only metrics not already recorded on this Tracker.
121+
/// Call <c>CreateTracker</c> on the AI Config to start a new run.
122+
/// </remarks>
89123
/// <param name="request">a task representing the request</param>
90124
/// <returns>the task</returns>
91125
public Task<Response> TrackRequest(Task<Response> request);
92126

93127
/// <summary>
94128
/// Tracks token usage related to this config.
95129
/// </summary>
130+
/// <remarks>Records at most once per Tracker; further calls are ignored.</remarks>
96131
/// <param name="usage">the token usage</param>
97132
public void TrackTokens(Usage usage);
98133
}

pkgs/sdk/server-ai/src/LdAiClient.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,11 @@ public LdAiCompletionConfig Config(string key, Context context, LdAiCompletionCo
7272
{
7373
return CompletionConfig(key, context, defaultValue, variables);
7474
}
75+
76+
77+
/// <inheritdoc/>
78+
public ILdAiConfigTracker CreateTracker(string resumptionToken, Context context)
79+
{
80+
return LdAiConfigTracker.FromResumptionToken(resumptionToken, _client, context);
81+
}
7582
}

0 commit comments

Comments
 (0)