Skip to content

Commit 0146f76

Browse files
authored
fix: Improve usage reporting (#353)
1 parent 509ab25 commit 0146f76

3 files changed

Lines changed: 122 additions & 17 deletions

File tree

ldai/client.go

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,40 +40,77 @@ type Client struct {
4040
logger interfaces.LDLoggers
4141
}
4242

43+
const (
44+
sdkInfoEvent = "$ld:ai:sdk:info"
45+
usageCompletionConfig = "$ld:ai:usage:completion-config"
46+
usageJudgeConfig = "$ld:ai:usage:judge-config"
47+
)
48+
4349
// NewClient creates a new AI Client. The provided SDK interface must not be nil. The client will use the provided SDK's
4450
// loggers to log warnings and errors.
4551
func NewClient(sdk ServerSDK) (*Client, error) {
4652
if sdk == nil {
4753
return nil, fmt.Errorf("sdk must not be nil")
4854
}
49-
return &Client{
55+
c := &Client{
5056
sdk: sdk,
5157
logger: sdk.Loggers(),
52-
}, nil
58+
}
59+
if err := c.trackSDKInfo(); err != nil {
60+
c.logger.Warnf("AI Client: failed to track SDK info: %v", err)
61+
}
62+
return c, nil
63+
}
64+
65+
func (c *Client) trackSDKInfo() error {
66+
ctx, err := ldcontext.NewBuilder("ld-internal-tracking").Kind("ld_ai").Anonymous(true).TryBuild()
67+
if err != nil {
68+
return err
69+
}
70+
data := ldvalue.ObjectBuild().
71+
Set("aiSdkName", ldvalue.String(SDKName)).
72+
Set("aiSdkVersion", ldvalue.String(Version)).
73+
Set("aiSdkLanguage", ldvalue.String(SDKLanguage)).
74+
Build()
75+
return c.sdk.TrackMetric(sdkInfoEvent, ctx, 1, data)
5376
}
5477

5578
func (c *Client) logConfigWarning(key string, format string, args ...interface{}) {
5679
prefix := "AI Config '" + key + "': "
5780
c.logger.Warnf(prefix+format, args...)
5881
}
5982

60-
// Config evaluates an AI Config named by a given key for the given context.
83+
// CompletionConfig retrieves and processes a Completion AI Config based on the provided key, LaunchDarkly context,
84+
// and variables. This includes the model configuration and the customized messages.
6185
//
6286
// The config's messages will undergo Mustache template interpolation using the provided variables, which may be
6387
// nil. If the config cannot be evaluated or LaunchDarkly is unreachable, the default value is returned. Note that
6488
// the messages in the default will not undergo template interpolation.
6589
//
6690
// To send analytic events to LaunchDarkly related to the AI Config, call methods on the returned Tracker.
67-
func (c *Client) Config(
91+
func (c *Client) CompletionConfig(
6892
key string,
6993
context ldcontext.Context,
7094
defaultValue Config,
7195
variables map[string]interface{},
7296
) (Config, *Tracker) {
73-
_ = c.sdk.TrackMetric("$ld:ai:config:function:single", context, 1, ldvalue.String(key))
97+
data := ldvalue.ObjectBuild().Set("configKey", ldvalue.String(key)).Build()
98+
_ = c.sdk.TrackMetric(usageCompletionConfig, context, 1, data)
7499
return c.evaluateConfig(key, context, defaultValue, variables)
75100
}
76101

102+
// Config evaluates an AI Config named by a given key for the given context.
103+
//
104+
// Deprecated: Use CompletionConfig instead.
105+
func (c *Client) Config(
106+
key string,
107+
context ldcontext.Context,
108+
defaultValue Config,
109+
variables map[string]interface{},
110+
) (Config, *Tracker) {
111+
return c.CompletionConfig(key, context, defaultValue, variables)
112+
}
113+
77114
// evaluateConfig fetches and interpolates an AI Config without emitting any metric.
78115
// Callers (Config, JudgeConfig) are meant to emit their own metric before calling this.
79116
func (c *Client) evaluateConfig(
@@ -189,21 +226,25 @@ func interpolateTemplate(template string, variables map[string]interface{}) (str
189226
return m.RenderString(variables)
190227
}
191228

192-
// JudgeConfig evaluates an AI Config, tracking it as a judge function. See Config for details.
229+
// JudgeConfig retrieves and processes a Judge AI Config based on the provided key, LaunchDarkly context, and
230+
// variables. This includes the model configuration and the customized messages for evaluation.
193231
//
194232
// This method extends the provided variables with reserved judge variables:
195233
// - "message_history": "{{message_history}}"
196234
// - "response_to_evaluate": "{{response_to_evaluate}}"
197235
//
198236
// These literal placeholder strings preserve the Mustache templates through the first interpolation
199237
// (during config fetch), allowing Judge.Evaluate() to perform a second interpolation with actual values.
238+
//
239+
// To send analytic events to LaunchDarkly related to the AI Config, call methods on the returned Tracker.
200240
func (c *Client) JudgeConfig(
201241
key string,
202242
context ldcontext.Context,
203243
defaultValue Config,
204244
variables map[string]interface{},
205245
) (Config, *Tracker) {
206-
_ = c.sdk.TrackMetric("$ld:ai:judge:function:single", context, 1, ldvalue.String(key))
246+
data := ldvalue.ObjectBuild().Set("configKey", ldvalue.String(key)).Build()
247+
_ = c.sdk.TrackMetric(usageJudgeConfig, context, 1, data)
207248

208249
// Extend variables with reserved judge placeholders
209250
extendedVariables := make(map[string]interface{})

ldai/client_test.go

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,24 @@ func TestNewClientReturnsErrorWhenSDKIsNil(t *testing.T) {
6767
}
6868

6969
func TestNewClient(t *testing.T) {
70-
client, err := NewClient(newMockSDK(nil, nil))
70+
mockSDK := newMockSDK(nil, nil)
71+
client, err := NewClient(mockSDK)
7172
require.NoError(t, err)
7273
require.NotNil(t, client)
74+
75+
// Verify SDK info event was fired on construction.
76+
require.Len(t, mockSDK.events, 1)
77+
evt := mockSDK.events[0]
78+
assert.Equal(t, "$ld:ai:sdk:info", evt.eventName)
79+
assert.Equal(t, float64(1), evt.metricValue)
80+
assert.Equal(t, "launchdarkly-go-server-sdk-ai", evt.data.GetByKey("aiSdkName").StringValue())
81+
assert.Equal(t, Version, evt.data.GetByKey("aiSdkVersion").StringValue())
82+
assert.Equal(t, "go", evt.data.GetByKey("aiSdkLanguage").StringValue())
83+
84+
// Verify the context is anonymous with kind ld_ai.
85+
assert.Equal(t, "ld-internal-tracking", evt.context.Key())
86+
assert.True(t, evt.context.Anonymous())
87+
assert.Equal(t, ldcontext.Kind("ld_ai"), evt.context.Kind())
7388
}
7489

7590
func TestEvalErrorReturnsDefault(t *testing.T) {
@@ -302,12 +317,48 @@ func TestCanSetDefaultConfigFields(t *testing.T) {
302317
assert.Equal(t, datamodel.System, msg[1].Role)
303318
}
304319

305-
func TestConfigMethodTracking(t *testing.T) {
320+
func TestCompletionConfigMethodTracking(t *testing.T) {
321+
mockSDK := newMockSDK(nil, nil)
322+
client, err := NewClient(mockSDK)
323+
require.NoError(t, err)
324+
require.NotNil(t, client)
325+
326+
// Clear the SDK info event from construction.
327+
mockSDK.events = nil
328+
329+
defaultConfig := NewConfig().WithEnabled(false).Build()
330+
context := ldcontext.New("user-key")
331+
configKey := "test-config-key"
332+
333+
config, tracker := client.CompletionConfig(configKey, context, defaultConfig, nil)
334+
335+
require.NotNil(t, config)
336+
require.NotNil(t, tracker)
337+
338+
expectedData := ldvalue.ObjectBuild().Set("configKey", ldvalue.String(configKey)).Build()
339+
expectedEvents := []mockEvent{
340+
{
341+
eventName: "$ld:ai:usage:completion-config",
342+
context: context,
343+
metricValue: 1,
344+
data: expectedData,
345+
},
346+
}
347+
348+
assert.ElementsMatch(t, expectedEvents, mockSDK.events)
349+
}
350+
351+
// TestDeprecatedConfigDelegatesToCompletionConfig verifies the deprecated Config method delegates
352+
// to CompletionConfig and emits the same usage event.
353+
func TestDeprecatedConfigDelegatesToCompletionConfig(t *testing.T) {
306354
mockSDK := newMockSDK(nil, nil)
307355
client, err := NewClient(mockSDK)
308356
require.NoError(t, err)
309357
require.NotNil(t, client)
310358

359+
// Clear the SDK info event from construction.
360+
mockSDK.events = nil
361+
311362
defaultConfig := NewConfig().WithEnabled(false).Build()
312363
context := ldcontext.New("user-key")
313364
configKey := "test-config-key"
@@ -317,20 +368,21 @@ func TestConfigMethodTracking(t *testing.T) {
317368
require.NotNil(t, config)
318369
require.NotNil(t, tracker)
319370

371+
expectedData := ldvalue.ObjectBuild().Set("configKey", ldvalue.String(configKey)).Build()
320372
expectedEvents := []mockEvent{
321373
{
322-
eventName: "$ld:ai:config:function:single",
374+
eventName: "$ld:ai:usage:completion-config",
323375
context: context,
324376
metricValue: 1,
325-
data: ldvalue.String(configKey),
377+
data: expectedData,
326378
},
327379
}
328380

329381
assert.ElementsMatch(t, expectedEvents, mockSDK.events)
330382
}
331383

332384
// TestJudgeConfigMethodTracking verifies that JudgeConfig emits only the judge metric,
333-
// not the config metric, so judge evaluations are not double-counted on the dashboard.
385+
// not the completion-config metric, so judge evaluations are not double-counted on the dashboard.
334386
func TestJudgeConfigMethodTracking(t *testing.T) {
335387
json := []byte(`{
336388
"_ldMeta": {"variationKey": "1", "enabled": true},
@@ -343,6 +395,9 @@ func TestJudgeConfigMethodTracking(t *testing.T) {
343395
require.NoError(t, err)
344396
require.NotNil(t, client)
345397

398+
// Clear the SDK info event from construction.
399+
mockSDK.events = nil
400+
346401
defaultConfig := Disabled()
347402
context := ldcontext.New("user-key")
348403
configKey := "judge-config-key"
@@ -353,16 +408,17 @@ func TestJudgeConfigMethodTracking(t *testing.T) {
353408
require.NotNil(t, tracker)
354409

355410
// Only the judge metric should be emitted; evaluateConfig does not emit any metric.
411+
expectedData := ldvalue.ObjectBuild().Set("configKey", ldvalue.String(configKey)).Build()
356412
expectedEvents := []mockEvent{
357413
{
358-
eventName: "$ld:ai:judge:function:single",
414+
eventName: "$ld:ai:usage:judge-config",
359415
context: context,
360416
metricValue: 1,
361-
data: ldvalue.String(configKey),
417+
data: expectedData,
362418
},
363419
}
364420
assert.ElementsMatch(t, expectedEvents, mockSDK.events,
365-
"JudgeConfig must not emit $ld:ai:config:function:single to avoid double-counting")
421+
"JudgeConfig must not emit $ld:ai:usage:completion-config to avoid double-counting")
366422
}
367423

368424
func TestCanSetModelParameters(t *testing.T) {

ldai/package_info.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
// Package ldai contains an AI SDK suitable for usage with generative AI applications.
22
package ldai
33

4-
// Version is the current version string of the ldai package. This is updated by our release scripts.
5-
const Version = "0.8.0" // {{ x-release-please-version }}
4+
const (
5+
// Version is the current version string of the ldai package. This is updated by our release scripts.
6+
Version = "0.8.0" // {{ x-release-please-version }}
7+
8+
// SDKName is the canonical name of this AI SDK package.
9+
SDKName = "launchdarkly-go-server-sdk-ai"
10+
11+
// SDKLanguage is the programming language of this AI SDK.
12+
SDKLanguage = "go"
13+
)

0 commit comments

Comments
 (0)