From 49e789e47e8342dc38bf249e231f6199cf90a9a2 Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Fri, 1 May 2026 15:24:10 -0500 Subject: [PATCH] chore: Convert model tools to a mapped list --- .../__tests__/LDAIClientImpl.test.ts | 77 ++++++++++++------- packages/sdk/server-ai/src/LDAIClientImpl.ts | 2 +- .../src/api/config/LDAIConfigUtils.ts | 75 ++++-------------- 3 files changed, 62 insertions(+), 92 deletions(-) diff --git a/packages/sdk/server-ai/__tests__/LDAIClientImpl.test.ts b/packages/sdk/server-ai/__tests__/LDAIClientImpl.test.ts index afa9500f16..8892fd9a30 100644 --- a/packages/sdk/server-ai/__tests__/LDAIClientImpl.test.ts +++ b/packages/sdk/server-ai/__tests__/LDAIClientImpl.test.ts @@ -916,40 +916,62 @@ describe('tools map support', () => { expect(result.tools).toBeUndefined(); }); - it('parses tools from model.parameters.tools when root tools is absent', async () => { + it('converts model.parameters.tools array to map when root tools is absent', async () => { const client = new LDAIClientImpl(mockLdClient); const key = 'test-flag'; const defaultValue: LDAICompletionConfigDefault = { enabled: false }; - const modelParamsTools = { - 'web-search-tool': { - name: 'web-search-tool', - type: 'function', - parameters: { type: 'object', properties: {}, required: [] }, - }, - }; const mockVariation = { - model: { name: 'example-model', parameters: { tools: modelParamsTools } }, + model: { + name: 'example-model', + parameters: { + tools: [ + { + name: 'search', + type: 'function', + description: 'Search the web', + parameters: { type: 'object', properties: {}, required: [] }, + }, + { + name: 'get_weather', + type: 'function', + description: 'Get weather for a location', + }, + ], + }, + }, _ldMeta: { variationKey: 'v1', enabled: true, mode: 'completion' }, }; mockLdClient.variation.mockResolvedValue(mockVariation); const result = await client.completionConfig(key, testContext, defaultValue); - expect(result.tools).toEqual(modelParamsTools); + expect(result.tools).toBeDefined(); + expect(result.tools!['search'].name).toBe('search'); + expect(result.tools!['search'].type).toBe('function'); + expect(result.tools!['search'].description).toBe('Search the web'); + expect(result.tools!['search'].parameters).toEqual({ + type: 'object', + properties: {}, + required: [], + }); + expect(result.tools!['get_weather'].name).toBe('get_weather'); + expect(result.tools!['get_weather'].description).toBe('Get weather for a location'); }); - it('uses root tools over model.parameters.tools when both are present', async () => { + it('uses root tools map over model.parameters.tools array when both are present', async () => { const client = new LDAIClientImpl(mockLdClient); const key = 'test-flag'; const defaultValue: LDAICompletionConfigDefault = { enabled: false }; const rootTools = { 'root-tool': { name: 'root-tool', type: 'function' }, }; - const modelParamsTools = { - 'params-tool': { name: 'params-tool', type: 'function' }, - }; const mockVariation = { - model: { name: 'example-model', parameters: { tools: modelParamsTools } }, + model: { + name: 'example-model', + parameters: { + tools: [{ name: 'params-tool', type: 'function' }], + }, + }, tools: rootTools, _ldMeta: { variationKey: 'v1', enabled: true, mode: 'completion' }, }; @@ -960,7 +982,7 @@ describe('tools map support', () => { expect(result.tools).toEqual(rootTools); }); - it('returns undefined when model.parameters.tools is an array', async () => { + it('skips model.parameters.tools array entries without a name', async () => { const client = new LDAIClientImpl(mockLdClient); const key = 'test-flag'; const defaultValue: LDAICompletionConfigDefault = { enabled: false }; @@ -968,7 +990,10 @@ describe('tools map support', () => { model: { name: 'example-model', parameters: { - tools: [{ type: 'function', function: { name: 'search', description: 'Search' } }], + tools: [ + { name: 'valid-tool', type: 'function' }, + { type: 'function', description: 'no name on this one' }, + ], }, }, _ldMeta: { variationKey: 'v1', enabled: true, mode: 'completion' }, @@ -977,22 +1002,19 @@ describe('tools map support', () => { const result = await client.completionConfig(key, testContext, defaultValue); - expect(result.tools).toBeUndefined(); + expect(result.tools).toBeDefined(); + expect(result.tools!['valid-tool'].name).toBe('valid-tool'); + expect(Object.keys(result.tools!)).toHaveLength(1); }); - it('falls back to key name for model.parameters.tools entries missing the name field', async () => { + it('returns undefined when model.parameters.tools is not an array', async () => { const client = new LDAIClientImpl(mockLdClient); const key = 'test-flag'; const defaultValue: LDAICompletionConfigDefault = { enabled: false }; const mockVariation = { model: { name: 'example-model', - parameters: { - tools: { - 'valid-tool': { name: 'valid-tool', type: 'function' }, - 'no-name-tool': { type: 'function' }, - }, - }, + parameters: { tools: 'not-an-array' }, }, _ldMeta: { variationKey: 'v1', enabled: true, mode: 'completion' }, }; @@ -1000,9 +1022,6 @@ describe('tools map support', () => { const result = await client.completionConfig(key, testContext, defaultValue); - expect(result.tools).toBeDefined(); - expect(result.tools!['valid-tool'].name).toBe('valid-tool'); - expect(result.tools!['no-name-tool']).toBeDefined(); - expect(result.tools!['no-name-tool'].name).toBe('no-name-tool'); + expect(result.tools).toBeUndefined(); }); }); diff --git a/packages/sdk/server-ai/src/LDAIClientImpl.ts b/packages/sdk/server-ai/src/LDAIClientImpl.ts index 1fbfe73c50..316a84e56d 100644 --- a/packages/sdk/server-ai/src/LDAIClientImpl.ts +++ b/packages/sdk/server-ai/src/LDAIClientImpl.ts @@ -108,7 +108,7 @@ export class LDAIClientImpl implements LDAIClient { graphKey, ); - const config = LDAIConfigUtils.fromFlagValue(key, value, trackerFactory, this._logger); + const config = LDAIConfigUtils.fromFlagValue(key, value, trackerFactory); // Apply variable interpolation (always needed for ldctx) return this._applyInterpolation(config, context, variables); diff --git a/packages/sdk/server-ai/src/api/config/LDAIConfigUtils.ts b/packages/sdk/server-ai/src/api/config/LDAIConfigUtils.ts index cf3910b504..093598bb2f 100644 --- a/packages/sdk/server-ai/src/api/config/LDAIConfigUtils.ts +++ b/packages/sdk/server-ai/src/api/config/LDAIConfigUtils.ts @@ -1,5 +1,3 @@ -import { LDLogger } from '@launchdarkly/js-server-sdk-common'; - import { LDAIConfigTracker } from './LDAIConfigTracker'; import { LDAIAgentConfig, @@ -98,7 +96,6 @@ export class LDAIConfigUtils { key: string, flagValue: LDAIConfigFlagValue, trackerFactory: () => LDAIConfigTracker, - logger?: LDLogger, ): LDAIConfigKind { // Determine the actual mode from flag value // eslint-disable-next-line no-underscore-dangle @@ -106,12 +103,12 @@ export class LDAIConfigUtils { switch (flagValueMode) { case 'agent': - return this.toAgentConfig(key, flagValue, trackerFactory, logger); + return this.toAgentConfig(key, flagValue, trackerFactory); case 'judge': return this.toJudgeConfig(key, flagValue, trackerFactory); case 'completion': default: - return this.toCompletionConfig(key, flagValue, trackerFactory, logger); + return this.toCompletionConfig(key, flagValue, trackerFactory); } } @@ -146,70 +143,26 @@ export class LDAIConfigUtils { } } - private static _parseToolsMap( - toolsMap: { [key: string]: unknown }, - logger: LDLogger | undefined, - ): { [toolName: string]: LDTool } { - const result: { [toolName: string]: LDTool } = {}; - for (const [toolName, toolValue] of Object.entries(toolsMap)) { - if (toolValue === null || typeof toolValue !== 'object' || Array.isArray(toolValue)) { - logger?.warn(`LaunchDarkly AI: Skipping tool "${toolName}": expected an object`); - continue; - } - const toolObj = toolValue as { [key: string]: unknown }; - result[toolName] = { - name: typeof toolObj['name'] === 'string' ? toolObj['name'] : toolName, - description: - typeof toolObj['description'] === 'string' ? toolObj['description'] : undefined, - type: typeof toolObj['type'] === 'string' ? toolObj['type'] : undefined, - parameters: - toolObj['parameters'] !== null && - typeof toolObj['parameters'] === 'object' && - !Array.isArray(toolObj['parameters']) - ? (toolObj['parameters'] as LDTool['parameters']) - : undefined, - customParameters: - toolObj['customParameters'] !== null && - typeof toolObj['customParameters'] === 'object' && - !Array.isArray(toolObj['customParameters']) - ? (toolObj['customParameters'] as LDTool['customParameters']) - : undefined, - }; - } - return result; - } - private static _resolveTools( flagValue: LDAIConfigFlagValue, - logger?: LDLogger, ): { [toolName: string]: LDTool } | undefined { if (flagValue.tools !== undefined) { - if ( - flagValue.tools === null || - typeof flagValue.tools !== 'object' || - Array.isArray(flagValue.tools) - ) { - logger?.warn( - `LaunchDarkly AI: Skipping tools: expected an object, got ${Array.isArray(flagValue.tools) ? 'array' : typeof flagValue.tools}`, - ); - return undefined; - } - const parsed = this._parseToolsMap(flagValue.tools as { [key: string]: unknown }, logger); - return Object.keys(parsed).length > 0 ? parsed : undefined; + return flagValue.tools as { [toolName: string]: LDTool } | undefined; } const rawTools = flagValue.model?.parameters?.['tools']; - - if (rawTools === null || rawTools === undefined) { + if (!Array.isArray(rawTools)) { return undefined; } - if (typeof rawTools !== 'object' || Array.isArray(rawTools)) { - return undefined; + const result: { [toolName: string]: LDTool } = {}; + for (const entry of rawTools) { + const tool = entry as LDTool; + if (tool?.name) { + result[tool.name] = tool; + } } - - const parsed = this._parseToolsMap(rawTools as { [key: string]: unknown }, logger); - return Object.keys(parsed).length > 0 ? parsed : undefined; + return Object.keys(result).length > 0 ? result : undefined; } /** @@ -240,14 +193,13 @@ export class LDAIConfigUtils { key: string, flagValue: LDAIConfigFlagValue, trackerFactory: () => LDAIConfigTracker, - logger?: LDLogger, ): LDAICompletionConfig { return { ...this._toBaseConfig(key, flagValue), createTracker: trackerFactory, messages: flagValue.messages, judgeConfiguration: flagValue.judgeConfiguration, - tools: this._resolveTools(flagValue, logger), + tools: this._resolveTools(flagValue), }; } @@ -263,14 +215,13 @@ export class LDAIConfigUtils { key: string, flagValue: LDAIConfigFlagValue, trackerFactory: () => LDAIConfigTracker, - logger?: LDLogger, ): LDAIAgentConfig { return { ...this._toBaseConfig(key, flagValue), createTracker: trackerFactory, instructions: flagValue.instructions, judgeConfiguration: flagValue.judgeConfiguration, - tools: this._resolveTools(flagValue, logger), + tools: this._resolveTools(flagValue), }; }