Skip to content

Commit af43b47

Browse files
committed
return the configs with trackers attached to stay consistent in ai sdk
1 parent 884bfc7 commit af43b47

File tree

6 files changed

+269
-284
lines changed

6 files changed

+269
-284
lines changed

packages/sdk/server-ai/src/LDAIClientImpl.ts

Lines changed: 84 additions & 171 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,14 @@ import { LDContext, LDLogger } from '@launchdarkly/js-server-sdk-common';
55
import { TrackedChat } from './api/chat';
66
import {
77
LDAIAgentConfig,
8+
LDAIAgentConfigDefault,
89
LDAIAgentRequestConfig,
9-
LDAIConfig,
10-
LDAIConfigKind,
11-
LDAIConfigTracker,
10+
LDAIConfigKindDefault,
11+
LDAIConversationConfig,
12+
LDAIConversationConfigDefault,
1213
LDAIJudgeConfig,
14+
LDAIJudgeConfigDefault,
1315
LDMessage,
14-
LDTrackedAgent,
15-
LDTrackedAgents,
16-
LDTrackedConfig,
17-
LDTrackedJudge,
1816
VercelAISDKConfig,
1917
VercelAISDKMapOptions,
2018
VercelAISDKProvider,
@@ -27,14 +25,6 @@ import { LDAIConfigMapper } from './LDAIConfigMapper';
2725
import { LDAIConfigTrackerImpl } from './LDAIConfigTrackerImpl';
2826
import { LDClientMin } from './LDClientMin';
2927

30-
/**
31-
* The result of evaluating a configuration.
32-
*/
33-
interface EvaluationResult {
34-
tracker: LDAIConfigTracker;
35-
config: LDAIConfigKind;
36-
}
37-
3828
export class LDAIClientImpl implements LDAIClient {
3929
private _logger?: LDLogger;
4030

@@ -49,14 +39,25 @@ export class LDAIClientImpl implements LDAIClient {
4939
private async _evaluate(
5040
key: string,
5141
context: LDContext,
52-
defaultValue: LDAIConfigKind,
53-
variables?: Record<string, any>,
54-
): Promise<EvaluationResult> {
42+
defaultValue: LDAIConfigKindDefault,
43+
mode: 'completion' | 'agent' | 'judge',
44+
variables?: Record<string, unknown>,
45+
): Promise<LDAIConversationConfig | LDAIAgentConfig | LDAIJudgeConfig> {
5546
// Convert default value to LDFlagValue format
56-
const ldFlagValue = LDAIConfigUtils.toFlagValue(defaultValue);
47+
const ldFlagValue = LDAIConfigUtils.toFlagValue(defaultValue, mode);
5748

5849
const value: LDAIConfigFlagValue = await this._ldClient.variation(key, context, ldFlagValue);
5950

51+
// Validate mode match
52+
// eslint-disable-next-line no-underscore-dangle
53+
const flagMode = value._ldMeta?.mode;
54+
if (flagMode !== mode) {
55+
this._logger?.warn(
56+
`AI Config mode mismatch for ${key}: expected ${mode}, got ${flagMode}. Returning disabled config.`,
57+
);
58+
return LDAIConfigUtils.createDisabledConfig(mode);
59+
}
60+
6061
const tracker = new LDAIConfigTrackerImpl(
6162
this._ldClient,
6263
key,
@@ -69,133 +70,96 @@ export class LDAIClientImpl implements LDAIClient {
6970
context,
7071
);
7172

72-
// Convert the flag value directly to the appropriate config type
73-
const config = LDAIConfigUtils.fromFlagValue(value);
73+
// Convert the flag value to the appropriate config type
74+
const config = LDAIConfigUtils.fromFlagValue(value, tracker);
7475

75-
// Apply variable interpolation if variables are provided
76-
if (variables) {
77-
const allVariables = { ...variables, ldctx: context };
76+
// Apply variable interpolation (always needed for ldctx)
77+
return this._applyInterpolation(config, context, variables);
78+
}
7879

79-
// Apply variable interpolation to messages if they exist
80-
if ('messages' in config && config.messages) {
81-
config.messages = config.messages.map((entry: LDMessage) => ({
80+
private _applyInterpolation(
81+
config: LDAIConversationConfig | LDAIAgentConfig | LDAIJudgeConfig,
82+
context: LDContext,
83+
variables?: Record<string, unknown>,
84+
): LDAIConversationConfig | LDAIAgentConfig | LDAIJudgeConfig {
85+
const allVariables = { ...variables, ldctx: context };
86+
87+
// Apply variable interpolation to messages if they exist
88+
if ('messages' in config && config.messages) {
89+
return {
90+
...config,
91+
messages: config.messages.map((entry: LDMessage) => ({
8292
...entry,
8393
content: this._interpolateTemplate(entry.content, allVariables),
84-
}));
85-
}
94+
})),
95+
};
96+
}
8697

87-
// Apply variable interpolation to instructions if they exist
88-
if ('instructions' in config && config.instructions) {
89-
config.instructions = this._interpolateTemplate(config.instructions, allVariables);
90-
}
98+
// Apply variable interpolation to instructions if they exist
99+
if ('instructions' in config && config.instructions) {
100+
return {
101+
...config,
102+
instructions: this._interpolateTemplate(config.instructions, allVariables),
103+
};
91104
}
92105

106+
return config;
107+
}
108+
109+
private _addVercelAISDKSupport(config: LDAIConversationConfig): LDAIConversationConfig {
110+
const { messages } = config;
111+
const mapper = new LDAIConfigMapper(config.model, config.provider, messages);
112+
93113
return {
94-
tracker,
95-
config,
114+
...config,
115+
toVercelAISDK: <TMod>(
116+
sdkProvider: VercelAISDKProvider<TMod> | Record<string, VercelAISDKProvider<TMod>>,
117+
options?: VercelAISDKMapOptions | undefined,
118+
): VercelAISDKConfig<TMod> => mapper.toVercelAISDK(sdkProvider, options),
96119
};
97120
}
98121

99122
async config(
100123
key: string,
101124
context: LDContext,
102-
defaultValue: LDAIConfigKind,
125+
defaultValue: LDAIConversationConfigDefault,
103126
variables?: Record<string, unknown>,
104-
): Promise<LDTrackedConfig> {
127+
): Promise<LDAIConversationConfig> {
105128
this._ldClient.track('$ld:ai:config:function:single', context, key, 1);
106129

107-
const { tracker, config } = await this._evaluate(key, context, defaultValue, variables);
108-
109-
// Cast to LDAIConfig since config method only accepts completion configs
110-
const completionConfig = config as LDAIConfig;
111-
112-
// Create the mapper for toVercelAISDK functionality
113-
const messages = 'messages' in completionConfig ? completionConfig.messages : undefined;
114-
const mapper = new LDAIConfigMapper(
115-
completionConfig.model,
116-
completionConfig.provider,
117-
messages,
118-
);
119-
120-
return {
121-
tracker,
122-
config: {
123-
...completionConfig,
124-
toVercelAISDK: <TMod>(
125-
sdkProvider: VercelAISDKProvider<TMod> | Record<string, VercelAISDKProvider<TMod>>,
126-
options?: VercelAISDKMapOptions | undefined,
127-
): VercelAISDKConfig<TMod> => mapper.toVercelAISDK(sdkProvider, options),
128-
},
129-
};
130+
const config = await this._evaluate(key, context, defaultValue, 'completion', variables);
131+
return this._addVercelAISDKSupport(config as LDAIConversationConfig);
130132
}
131133

132134
async judge(
133135
key: string,
134136
context: LDContext,
135-
defaultValue: LDAIJudgeConfig,
137+
defaultValue: LDAIJudgeConfigDefault,
136138
variables?: Record<string, unknown>,
137-
): Promise<LDTrackedJudge> {
139+
): Promise<LDAIJudgeConfig> {
138140
this._ldClient.track('$ld:ai:judge:function:single', context, key, 1);
139141

140-
const { tracker, config } = await this._evaluate(key, context, defaultValue, variables);
141-
142-
// Cast to judge config since this method only accepts judge configs
143-
const judgeConfig = config as LDAIJudgeConfig;
144-
145-
// Create the mapper for toVercelAISDK functionality
146-
const messages = 'messages' in judgeConfig ? judgeConfig.messages : undefined;
147-
const mapper = new LDAIConfigMapper(judgeConfig.model, judgeConfig.provider, messages);
148-
149-
return {
150-
tracker,
151-
judge: {
152-
...judgeConfig,
153-
toVercelAISDK: <TMod>(
154-
sdkProvider: VercelAISDKProvider<TMod> | Record<string, VercelAISDKProvider<TMod>>,
155-
options?: VercelAISDKMapOptions | undefined,
156-
): VercelAISDKConfig<TMod> => mapper.toVercelAISDK(sdkProvider, options),
157-
},
158-
};
142+
const config = await this._evaluate(key, context, defaultValue, 'judge', variables);
143+
return config as LDAIJudgeConfig;
159144
}
160145

161146
async agent(
162147
key: string,
163148
context: LDContext,
164-
defaultValue: LDAIAgentConfig,
149+
defaultValue: LDAIAgentConfigDefault,
165150
variables?: Record<string, unknown>,
166-
): Promise<LDTrackedAgent> {
151+
): Promise<LDAIAgentConfig> {
167152
// Track agent usage
168153
this._ldClient.track('$ld:ai:agent:function:single', context, key, 1);
169154

170-
const { tracker, config } = await this._evaluate(key, context, defaultValue, variables);
171-
172-
// Check if we have an agent config, log warning and return disabled config if not
173-
if (config.mode !== 'agent') {
174-
this._logger?.warn(
175-
`Configuration is not an agent (mode: ${config.mode}), returning disabled config`,
176-
);
177-
return {
178-
tracker,
179-
agent: {
180-
...config,
181-
enabled: false,
182-
mode: 'agent' as const,
183-
} as LDAIAgentConfig,
184-
};
185-
}
186-
187-
const agent = config as LDAIAgentConfig;
188-
189-
return {
190-
tracker,
191-
agent,
192-
};
155+
const config = await this._evaluate(key, context, defaultValue, 'agent', variables);
156+
return config as LDAIAgentConfig;
193157
}
194158

195159
async agents<const T extends readonly LDAIAgentRequestConfig[]>(
196160
agentConfigs: T,
197161
context: LDContext,
198-
): Promise<LDTrackedAgents> {
162+
): Promise<Record<T[number]['key'], LDAIAgentConfig>> {
199163
// Track multiple agents usage
200164
this._ldClient.track(
201165
'$ld:ai:agent:function:multiple',
@@ -205,90 +169,55 @@ export class LDAIClientImpl implements LDAIClient {
205169
);
206170

207171
const agents = {} as Record<T[number]['key'], LDAIAgentConfig>;
208-
let tracker: LDAIConfigTracker | undefined;
209172

210173
await Promise.all(
211174
agentConfigs.map(async (config) => {
212-
const result = await this._evaluate(
175+
const agent = await this._evaluate(
213176
config.key,
214177
context,
215178
config.defaultValue,
179+
'agent',
216180
config.variables,
217181
);
218-
219-
// Check if we have an agent config, log warning and return disabled config if not
220-
if (result.config.mode !== 'agent') {
221-
this._logger?.warn(
222-
`Configuration ${config.key} is not an agent (mode: ${result.config.mode}), returning disabled config`,
223-
);
224-
agents[config.key as T[number]['key']] = {
225-
...result.config,
226-
enabled: false,
227-
mode: 'agent' as const,
228-
} as LDAIAgentConfig;
229-
if (!tracker) {
230-
tracker = result.tracker;
231-
}
232-
return;
233-
}
234-
235-
const agent = result.config as LDAIAgentConfig;
236-
agents[config.key as T[number]['key']] = agent;
237-
if (!tracker) {
238-
tracker = result.tracker;
239-
}
182+
agents[config.key as T[number]['key']] = agent as LDAIAgentConfig;
240183
}),
241184
);
242185

243-
return {
244-
tracker: tracker!,
245-
agents,
246-
};
186+
return agents;
247187
}
248188

249189
async initChat(
250190
key: string,
251191
context: LDContext,
252-
defaultValue: LDAIConfig,
192+
defaultValue: LDAIConversationConfigDefault,
253193
variables?: Record<string, unknown>,
254194
defaultAiProvider?: SupportedAIProvider,
255195
): Promise<TrackedChat | undefined> {
256196
// Track chat initialization
257197
this._ldClient.track('$ld:ai:config:function:initChat', context, key, 1);
258198

259-
const result = await this.config(key, context, defaultValue, variables);
199+
const config = await this.config(key, context, defaultValue, variables);
260200

261201
// Return undefined if the configuration is disabled
262-
if (!result.config.enabled) {
202+
if (!config.enabled || !config.tracker) {
263203
this._logger?.info(`Chat configuration is disabled: ${key}`);
264204
return undefined;
265205
}
266206

267-
// Check if we have a completion config, log warning and return undefined if not
268-
if (result.config.mode && result.config.mode !== 'completion') {
269-
this._logger?.warn(
270-
`Configuration ${key} is not a completion config (mode: ${result.config.mode}), returning undefined`,
271-
);
272-
return undefined;
273-
}
274-
275-
// Cast to LDAIConfig since initChat only accepts completion configs
276-
const config = result.config as LDAIConfig;
277-
278207
// Create the AIProvider instance
279208
const provider = await AIProviderFactory.create(config, this._logger, defaultAiProvider);
280209
if (!provider) {
281210
return undefined;
282211
}
283212

284213
// Create the TrackedChat instance with the provider
285-
return new TrackedChat(config, result.tracker, provider);
214+
return new TrackedChat(config, config.tracker, provider);
286215
}
287216

288217
async initJudge(
289218
key: string,
290219
context: LDContext,
291-
defaultValue: LDAIJudgeConfig,
220+
defaultValue: LDAIJudgeConfigDefault,
292221
variables?: Record<string, unknown>,
293222
defaultAiProvider?: SupportedAIProvider,
294223
): Promise<Judge | undefined> {
@@ -297,38 +226,22 @@ export class LDAIClientImpl implements LDAIClient {
297226

298227
try {
299228
// Retrieve the judge AI Config using the new judge method
300-
const result = await this.judge(key, context, defaultValue, variables);
229+
const judgeConfig = await this.judge(key, context, defaultValue, variables);
301230

302231
// Return undefined if the configuration is disabled
303-
if (!result.judge.enabled) {
232+
if (!judgeConfig.enabled || !judgeConfig.tracker) {
304233
this._logger?.info(`Judge configuration is disabled: ${key}`);
305234
return undefined;
306235
}
307236

308-
// Validate that this is a judge configuration
309-
if (result.judge.mode !== 'judge') {
310-
this._logger?.warn(`Configuration ${key} is not a judge (mode: ${result.judge.mode})`);
311-
return undefined;
312-
}
313-
314-
// Validate that the judge configuration has the required evaluation metric keys
315-
if (!result.judge.evaluationMetricKeys || result.judge.evaluationMetricKeys.length === 0) {
316-
this._logger?.error(`Judge configuration ${key} is missing required evaluationMetricKeys`);
317-
return undefined;
318-
}
319-
320237
// Create the AIProvider instance
321-
const provider = await AIProviderFactory.create(
322-
result.judge,
323-
this._logger,
324-
defaultAiProvider,
325-
);
238+
const provider = await AIProviderFactory.create(judgeConfig, this._logger, defaultAiProvider);
326239
if (!provider) {
327240
return undefined;
328241
}
329242

330243
// Create and return the Judge instance
331-
return new Judge(result.judge, result.tracker, provider, this._logger);
244+
return new Judge(judgeConfig, judgeConfig.tracker, provider, this._logger);
332245
} catch (error) {
333246
this._logger?.error(`Failed to initialize judge ${key}:`, error);
334247
return undefined;

0 commit comments

Comments
 (0)