Skip to content

Commit 32ffda1

Browse files
authored
chore: Convert model tools to a mapped list (#1350)
1 parent c08b9f7 commit 32ffda1

3 files changed

Lines changed: 62 additions & 92 deletions

File tree

packages/sdk/server-ai/__tests__/LDAIClientImpl.test.ts

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -916,40 +916,62 @@ describe('tools map support', () => {
916916
expect(result.tools).toBeUndefined();
917917
});
918918

919-
it('parses tools from model.parameters.tools when root tools is absent', async () => {
919+
it('converts model.parameters.tools array to map when root tools is absent', async () => {
920920
const client = new LDAIClientImpl(mockLdClient);
921921
const key = 'test-flag';
922922
const defaultValue: LDAICompletionConfigDefault = { enabled: false };
923-
const modelParamsTools = {
924-
'web-search-tool': {
925-
name: 'web-search-tool',
926-
type: 'function',
927-
parameters: { type: 'object', properties: {}, required: [] },
928-
},
929-
};
930923
const mockVariation = {
931-
model: { name: 'example-model', parameters: { tools: modelParamsTools } },
924+
model: {
925+
name: 'example-model',
926+
parameters: {
927+
tools: [
928+
{
929+
name: 'search',
930+
type: 'function',
931+
description: 'Search the web',
932+
parameters: { type: 'object', properties: {}, required: [] },
933+
},
934+
{
935+
name: 'get_weather',
936+
type: 'function',
937+
description: 'Get weather for a location',
938+
},
939+
],
940+
},
941+
},
932942
_ldMeta: { variationKey: 'v1', enabled: true, mode: 'completion' },
933943
};
934944
mockLdClient.variation.mockResolvedValue(mockVariation);
935945

936946
const result = await client.completionConfig(key, testContext, defaultValue);
937947

938-
expect(result.tools).toEqual(modelParamsTools);
948+
expect(result.tools).toBeDefined();
949+
expect(result.tools!['search'].name).toBe('search');
950+
expect(result.tools!['search'].type).toBe('function');
951+
expect(result.tools!['search'].description).toBe('Search the web');
952+
expect(result.tools!['search'].parameters).toEqual({
953+
type: 'object',
954+
properties: {},
955+
required: [],
956+
});
957+
expect(result.tools!['get_weather'].name).toBe('get_weather');
958+
expect(result.tools!['get_weather'].description).toBe('Get weather for a location');
939959
});
940960

941-
it('uses root tools over model.parameters.tools when both are present', async () => {
961+
it('uses root tools map over model.parameters.tools array when both are present', async () => {
942962
const client = new LDAIClientImpl(mockLdClient);
943963
const key = 'test-flag';
944964
const defaultValue: LDAICompletionConfigDefault = { enabled: false };
945965
const rootTools = {
946966
'root-tool': { name: 'root-tool', type: 'function' },
947967
};
948-
const modelParamsTools = {
949-
'params-tool': { name: 'params-tool', type: 'function' },
950-
};
951968
const mockVariation = {
952-
model: { name: 'example-model', parameters: { tools: modelParamsTools } },
969+
model: {
970+
name: 'example-model',
971+
parameters: {
972+
tools: [{ name: 'params-tool', type: 'function' }],
973+
},
974+
},
953975
tools: rootTools,
954976
_ldMeta: { variationKey: 'v1', enabled: true, mode: 'completion' },
955977
};
@@ -960,15 +982,18 @@ describe('tools map support', () => {
960982
expect(result.tools).toEqual(rootTools);
961983
});
962984

963-
it('returns undefined when model.parameters.tools is an array', async () => {
985+
it('skips model.parameters.tools array entries without a name', async () => {
964986
const client = new LDAIClientImpl(mockLdClient);
965987
const key = 'test-flag';
966988
const defaultValue: LDAICompletionConfigDefault = { enabled: false };
967989
const mockVariation = {
968990
model: {
969991
name: 'example-model',
970992
parameters: {
971-
tools: [{ type: 'function', function: { name: 'search', description: 'Search' } }],
993+
tools: [
994+
{ name: 'valid-tool', type: 'function' },
995+
{ type: 'function', description: 'no name on this one' },
996+
],
972997
},
973998
},
974999
_ldMeta: { variationKey: 'v1', enabled: true, mode: 'completion' },
@@ -977,32 +1002,26 @@ describe('tools map support', () => {
9771002

9781003
const result = await client.completionConfig(key, testContext, defaultValue);
9791004

980-
expect(result.tools).toBeUndefined();
1005+
expect(result.tools).toBeDefined();
1006+
expect(result.tools!['valid-tool'].name).toBe('valid-tool');
1007+
expect(Object.keys(result.tools!)).toHaveLength(1);
9811008
});
9821009

983-
it('falls back to key name for model.parameters.tools entries missing the name field', async () => {
1010+
it('returns undefined when model.parameters.tools is not an array', async () => {
9841011
const client = new LDAIClientImpl(mockLdClient);
9851012
const key = 'test-flag';
9861013
const defaultValue: LDAICompletionConfigDefault = { enabled: false };
9871014
const mockVariation = {
9881015
model: {
9891016
name: 'example-model',
990-
parameters: {
991-
tools: {
992-
'valid-tool': { name: 'valid-tool', type: 'function' },
993-
'no-name-tool': { type: 'function' },
994-
},
995-
},
1017+
parameters: { tools: 'not-an-array' },
9961018
},
9971019
_ldMeta: { variationKey: 'v1', enabled: true, mode: 'completion' },
9981020
};
9991021
mockLdClient.variation.mockResolvedValue(mockVariation);
10001022

10011023
const result = await client.completionConfig(key, testContext, defaultValue);
10021024

1003-
expect(result.tools).toBeDefined();
1004-
expect(result.tools!['valid-tool'].name).toBe('valid-tool');
1005-
expect(result.tools!['no-name-tool']).toBeDefined();
1006-
expect(result.tools!['no-name-tool'].name).toBe('no-name-tool');
1025+
expect(result.tools).toBeUndefined();
10071026
});
10081027
});

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export class LDAIClientImpl implements LDAIClient {
108108
graphKey,
109109
);
110110

111-
const config = LDAIConfigUtils.fromFlagValue(key, value, trackerFactory, this._logger);
111+
const config = LDAIConfigUtils.fromFlagValue(key, value, trackerFactory);
112112

113113
// Apply variable interpolation (always needed for ldctx)
114114
return this._applyInterpolation(config, context, variables);

packages/sdk/server-ai/src/api/config/LDAIConfigUtils.ts

Lines changed: 13 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { LDLogger } from '@launchdarkly/js-server-sdk-common';
2-
31
import { LDAIConfigTracker } from './LDAIConfigTracker';
42
import {
53
LDAIAgentConfig,
@@ -98,20 +96,19 @@ export class LDAIConfigUtils {
9896
key: string,
9997
flagValue: LDAIConfigFlagValue,
10098
trackerFactory: () => LDAIConfigTracker,
101-
logger?: LDLogger,
10299
): LDAIConfigKind {
103100
// Determine the actual mode from flag value
104101
// eslint-disable-next-line no-underscore-dangle
105102
const flagValueMode = flagValue._ldMeta?.mode;
106103

107104
switch (flagValueMode) {
108105
case 'agent':
109-
return this.toAgentConfig(key, flagValue, trackerFactory, logger);
106+
return this.toAgentConfig(key, flagValue, trackerFactory);
110107
case 'judge':
111108
return this.toJudgeConfig(key, flagValue, trackerFactory);
112109
case 'completion':
113110
default:
114-
return this.toCompletionConfig(key, flagValue, trackerFactory, logger);
111+
return this.toCompletionConfig(key, flagValue, trackerFactory);
115112
}
116113
}
117114

@@ -146,70 +143,26 @@ export class LDAIConfigUtils {
146143
}
147144
}
148145

149-
private static _parseToolsMap(
150-
toolsMap: { [key: string]: unknown },
151-
logger: LDLogger | undefined,
152-
): { [toolName: string]: LDTool } {
153-
const result: { [toolName: string]: LDTool } = {};
154-
for (const [toolName, toolValue] of Object.entries(toolsMap)) {
155-
if (toolValue === null || typeof toolValue !== 'object' || Array.isArray(toolValue)) {
156-
logger?.warn(`LaunchDarkly AI: Skipping tool "${toolName}": expected an object`);
157-
continue;
158-
}
159-
const toolObj = toolValue as { [key: string]: unknown };
160-
result[toolName] = {
161-
name: typeof toolObj['name'] === 'string' ? toolObj['name'] : toolName,
162-
description:
163-
typeof toolObj['description'] === 'string' ? toolObj['description'] : undefined,
164-
type: typeof toolObj['type'] === 'string' ? toolObj['type'] : undefined,
165-
parameters:
166-
toolObj['parameters'] !== null &&
167-
typeof toolObj['parameters'] === 'object' &&
168-
!Array.isArray(toolObj['parameters'])
169-
? (toolObj['parameters'] as LDTool['parameters'])
170-
: undefined,
171-
customParameters:
172-
toolObj['customParameters'] !== null &&
173-
typeof toolObj['customParameters'] === 'object' &&
174-
!Array.isArray(toolObj['customParameters'])
175-
? (toolObj['customParameters'] as LDTool['customParameters'])
176-
: undefined,
177-
};
178-
}
179-
return result;
180-
}
181-
182146
private static _resolveTools(
183147
flagValue: LDAIConfigFlagValue,
184-
logger?: LDLogger,
185148
): { [toolName: string]: LDTool } | undefined {
186149
if (flagValue.tools !== undefined) {
187-
if (
188-
flagValue.tools === null ||
189-
typeof flagValue.tools !== 'object' ||
190-
Array.isArray(flagValue.tools)
191-
) {
192-
logger?.warn(
193-
`LaunchDarkly AI: Skipping tools: expected an object, got ${Array.isArray(flagValue.tools) ? 'array' : typeof flagValue.tools}`,
194-
);
195-
return undefined;
196-
}
197-
const parsed = this._parseToolsMap(flagValue.tools as { [key: string]: unknown }, logger);
198-
return Object.keys(parsed).length > 0 ? parsed : undefined;
150+
return flagValue.tools as { [toolName: string]: LDTool } | undefined;
199151
}
200152

201153
const rawTools = flagValue.model?.parameters?.['tools'];
202-
203-
if (rawTools === null || rawTools === undefined) {
154+
if (!Array.isArray(rawTools)) {
204155
return undefined;
205156
}
206157

207-
if (typeof rawTools !== 'object' || Array.isArray(rawTools)) {
208-
return undefined;
158+
const result: { [toolName: string]: LDTool } = {};
159+
for (const entry of rawTools) {
160+
const tool = entry as LDTool;
161+
if (tool?.name) {
162+
result[tool.name] = tool;
163+
}
209164
}
210-
211-
const parsed = this._parseToolsMap(rawTools as { [key: string]: unknown }, logger);
212-
return Object.keys(parsed).length > 0 ? parsed : undefined;
165+
return Object.keys(result).length > 0 ? result : undefined;
213166
}
214167

215168
/**
@@ -240,14 +193,13 @@ export class LDAIConfigUtils {
240193
key: string,
241194
flagValue: LDAIConfigFlagValue,
242195
trackerFactory: () => LDAIConfigTracker,
243-
logger?: LDLogger,
244196
): LDAICompletionConfig {
245197
return {
246198
...this._toBaseConfig(key, flagValue),
247199
createTracker: trackerFactory,
248200
messages: flagValue.messages,
249201
judgeConfiguration: flagValue.judgeConfiguration,
250-
tools: this._resolveTools(flagValue, logger),
202+
tools: this._resolveTools(flagValue),
251203
};
252204
}
253205

@@ -263,14 +215,13 @@ export class LDAIConfigUtils {
263215
key: string,
264216
flagValue: LDAIConfigFlagValue,
265217
trackerFactory: () => LDAIConfigTracker,
266-
logger?: LDLogger,
267218
): LDAIAgentConfig {
268219
return {
269220
...this._toBaseConfig(key, flagValue),
270221
createTracker: trackerFactory,
271222
instructions: flagValue.instructions,
272223
judgeConfiguration: flagValue.judgeConfiguration,
273-
tools: this._resolveTools(flagValue, logger),
224+
tools: this._resolveTools(flagValue),
274225
};
275226
}
276227

0 commit comments

Comments
 (0)