From cd7e3eb5ad2857cf350535d262c528ff0a18ee44 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 8 Jul 2026 04:19:23 +0000 Subject: [PATCH] feat(server-ai): stamp modelKey and modelVersion on AI usage events (AIC-2858) Read modelKey and modelVersion from the AI Config variation payload and stamp them on emitted metric events alongside existing modelName and providerName fields. Default modelVersion to 1 when absent and exclude both fields from the resumption token. Additive and backward compatible. Co-authored-by: Anthony Torns II --- .../__tests__/LDAIClientImpl.test.ts | 85 ++++++++++++++ .../__tests__/LDAIConfigTrackerImpl.test.ts | 106 ++++++++++++++++++ packages/sdk/server-ai/src/LDAIClientImpl.ts | 2 + .../server-ai/src/LDAIConfigTrackerImpl.ts | 6 + .../src/api/config/LDAIConfigTracker.ts | 2 + .../sdk/server-ai/src/api/config/types.ts | 11 ++ 6 files changed, 212 insertions(+) diff --git a/packages/sdk/server-ai/__tests__/LDAIClientImpl.test.ts b/packages/sdk/server-ai/__tests__/LDAIClientImpl.test.ts index 86c1a0756b..66b0b7c551 100644 --- a/packages/sdk/server-ai/__tests__/LDAIClientImpl.test.ts +++ b/packages/sdk/server-ai/__tests__/LDAIClientImpl.test.ts @@ -846,6 +846,91 @@ describe('createTracker method', () => { configKey: 'my-config', variationKey: 'v1', version: 3, + modelVersion: 1, + }); + expect('modelKey' in tracker.getTrackData()).toBe(false); + }); +}); + +describe('modelKey and modelVersion tracking', () => { + it('stamps modelKey and modelVersion on tracker from flag payload', async () => { + const client = new LDAIClientImpl(mockLdClient); + const key = 'test-flag'; + + mockLdClient.variation.mockResolvedValue({ + model: { + name: 'gpt-4', + modelKey: 'my-model', + modelVersion: 2, + }, + provider: { name: 'openai' }, + messages: [], + _ldMeta: { + variationKey: 'v1', + enabled: true, + mode: 'completion', + version: 7, + }, + }); + + const result = await client.completionConfig(key, testContext); + const tracker = result.createTracker(); + + expect(tracker.getTrackData()).toMatchObject({ + modelKey: 'my-model', + modelVersion: 2, + }); + }); + + it('defaults modelVersion to 1 and omits modelKey when absent from flag payload', async () => { + const client = new LDAIClientImpl(mockLdClient); + const key = 'test-flag'; + + mockLdClient.variation.mockResolvedValue({ + model: { name: 'gpt-4' }, + provider: { name: 'openai' }, + messages: [], + _ldMeta: { + variationKey: 'v1', + enabled: true, + mode: 'completion', + version: 7, + }, + }); + + const result = await client.completionConfig(key, testContext); + const tracker = result.createTracker(); + + const trackData = tracker.getTrackData(); + expect('modelKey' in trackData).toBe(false); + expect(trackData.modelVersion).toBe(1); + }); + + it('exposes modelKey and modelVersion on config.model from flag payload', async () => { + const client = new LDAIClientImpl(mockLdClient); + const key = 'test-flag'; + + mockLdClient.variation.mockResolvedValue({ + model: { + name: 'gpt-4', + modelKey: 'my-model', + modelVersion: 2, + }, + provider: { name: 'openai' }, + messages: [], + _ldMeta: { + variationKey: 'v1', + enabled: true, + mode: 'completion', + }, + }); + + const result = await client.completionConfig(key, testContext); + + expect(result.model).toEqual({ + name: 'gpt-4', + modelKey: 'my-model', + modelVersion: 2, }); }); }); diff --git a/packages/sdk/server-ai/__tests__/LDAIConfigTrackerImpl.test.ts b/packages/sdk/server-ai/__tests__/LDAIConfigTrackerImpl.test.ts index 4b625f1bca..ab06f7147c 100644 --- a/packages/sdk/server-ai/__tests__/LDAIConfigTrackerImpl.test.ts +++ b/packages/sdk/server-ai/__tests__/LDAIConfigTrackerImpl.test.ts @@ -31,6 +31,7 @@ const getExpectedTrackData = () => ({ version, modelName, providerName, + modelVersion: 1, runId: testRunId, }); @@ -1193,3 +1194,108 @@ describe('fromResumptionToken', () => { expect('graphKey' in reconstructed.getTrackData()).toBe(false); }); }); + +it('includes modelKey and modelVersion in getTrackData when set on constructor', () => { + const tracker = new LDAIConfigTrackerImpl( + mockLdClient, + testRunId, + configKey, + variationKey, + version, + modelName, + providerName, + testContext, + undefined, + 'my-model', + 2, + ); + + expect(tracker.getTrackData()).toEqual({ + ...getExpectedTrackData(), + modelKey: 'my-model', + modelVersion: 2, + }); +}); + +it('omits modelKey from getTrackData when unset', () => { + const tracker = new LDAIConfigTrackerImpl( + mockLdClient, + testRunId, + configKey, + variationKey, + version, + modelName, + providerName, + testContext, + ); + + const trackData = tracker.getTrackData(); + expect('modelKey' in trackData).toBe(false); + expect(trackData.modelVersion).toBe(1); +}); + +it('omits modelKey from getTrackData when empty string', () => { + const tracker = new LDAIConfigTrackerImpl( + mockLdClient, + testRunId, + configKey, + variationKey, + version, + modelName, + providerName, + testContext, + undefined, + '', + 3, + ); + + const trackData = tracker.getTrackData(); + expect('modelKey' in trackData).toBe(false); + expect(trackData.modelVersion).toBe(3); +}); + +it('excludes modelKey and modelVersion from resumption token', () => { + const tracker = new LDAIConfigTrackerImpl( + mockLdClient, + testRunId, + configKey, + variationKey, + version, + modelName, + providerName, + testContext, + undefined, + 'my-model', + 2, + ); + + const decoded = JSON.parse(Buffer.from(tracker.resumptionToken, 'base64url').toString('utf8')); + expect('modelKey' in decoded).toBe(false); + expect('modelVersion' in decoded).toBe(false); +}); + +it('fromResumptionToken defaults modelVersion to 1 and omits modelKey', () => { + const original = new LDAIConfigTrackerImpl( + mockLdClient, + testRunId, + configKey, + variationKey, + version, + modelName, + providerName, + testContext, + undefined, + 'my-model', + 2, + ); + + const reconstructed = LDAIConfigTrackerImpl.fromResumptionToken( + original.resumptionToken, + mockLdClient, + testContext, + ); + + const trackData = reconstructed.getTrackData(); + expect('modelKey' in trackData).toBe(false); + expect(trackData.modelVersion).toBe(1); +}); diff --git a/packages/sdk/server-ai/src/LDAIClientImpl.ts b/packages/sdk/server-ai/src/LDAIClientImpl.ts index 708fbfc110..f7360d6fc2 100644 --- a/packages/sdk/server-ai/src/LDAIClientImpl.ts +++ b/packages/sdk/server-ai/src/LDAIClientImpl.ts @@ -106,6 +106,8 @@ export class LDAIClientImpl implements LDAIClient { value.provider?.name ?? '', context, graphKey, + value.model?.modelKey, + value.model?.modelVersion ?? 1, ); // Validate mode match diff --git a/packages/sdk/server-ai/src/LDAIConfigTrackerImpl.ts b/packages/sdk/server-ai/src/LDAIConfigTrackerImpl.ts index ab5a6fb918..e400d86a6f 100644 --- a/packages/sdk/server-ai/src/LDAIConfigTrackerImpl.ts +++ b/packages/sdk/server-ai/src/LDAIConfigTrackerImpl.ts @@ -19,6 +19,8 @@ export class LDAIConfigTrackerImpl implements LDAIConfigTracker { private _providerName: string, private _context: LDContext, private _graphKey?: string, + private _modelKey?: string, + private _modelVersion: number = 1, ) { this._trackedMetrics.resumptionToken = this.resumptionToken; } @@ -30,6 +32,8 @@ export class LDAIConfigTrackerImpl implements LDAIConfigTracker { version: number; modelName: string; providerName: string; + modelVersion: number; + modelKey?: string; graphKey?: string; } { return { @@ -39,6 +43,8 @@ export class LDAIConfigTrackerImpl implements LDAIConfigTracker { version: this._version, modelName: this._modelName, providerName: this._providerName, + modelVersion: this._modelVersion, + ...(this._modelKey ? { modelKey: this._modelKey } : {}), ...(this._graphKey !== undefined ? { graphKey: this._graphKey } : {}), }; } diff --git a/packages/sdk/server-ai/src/api/config/LDAIConfigTracker.ts b/packages/sdk/server-ai/src/api/config/LDAIConfigTracker.ts index 859cfd385f..4eabe504f5 100644 --- a/packages/sdk/server-ai/src/api/config/LDAIConfigTracker.ts +++ b/packages/sdk/server-ai/src/api/config/LDAIConfigTracker.ts @@ -22,6 +22,8 @@ export interface LDAIConfigTracker { version: number; modelName: string; providerName: string; + modelVersion: number; + modelKey?: string; graphKey?: string; }; diff --git a/packages/sdk/server-ai/src/api/config/types.ts b/packages/sdk/server-ai/src/api/config/types.ts index adcb3c4d9f..d966327125 100644 --- a/packages/sdk/server-ai/src/api/config/types.ts +++ b/packages/sdk/server-ai/src/api/config/types.ts @@ -42,6 +42,17 @@ export interface LDModelConfig { * The region for the model. */ region?: string; + + /** + * The stable, unique key of the model (used for direct lookup; distinct from + * `name`, which is not guaranteed unique). + */ + modelKey?: string; + + /** + * The pinned version of the model that this config variation references. + */ + modelVersion?: number; } export interface LDProviderConfig {