diff --git a/packages/sdk/server-ai/src/ldai/client.py b/packages/sdk/server-ai/src/ldai/client.py index 4fac06c..7d69429 100644 --- a/packages/sdk/server-ai/src/ldai/client.py +++ b/packages/sdk/server-ai/src/ldai/client.py @@ -53,30 +53,22 @@ _DISABLED_JUDGE_DEFAULT = AIJudgeConfigDefault.disabled() -def _parse_tools(tools_data: Optional[Dict[str, Any]]) -> Optional[Dict[str, LDTool]]: - """Parse the root-level tools map from a flag variation dict.""" - if not isinstance(tools_data, dict): - if tools_data is not None: - log.warning('Skipping tools: expected a dict, got %s', type(tools_data).__name__) - return None - result: Dict[str, LDTool] = {} - for tool_name, tool_dict in tools_data.items(): - if not isinstance(tool_dict, dict): - log.warning('Skipping tool "%s": expected a dict, got %s', tool_name, type(tool_dict).__name__) - continue - result[tool_name] = LDTool( - name=tool_dict.get('name', tool_name), - description=tool_dict.get('description'), - type=tool_dict.get('type'), - parameters=tool_dict.get('parameters'), - custom_parameters=tool_dict.get('customParameters'), - ) - return result or None - - def _resolve_tools(variation: Dict[str, Any]) -> Optional[Dict[str, LDTool]]: if 'tools' in variation: - return _parse_tools(variation['tools']) + tools_data = variation['tools'] + if not isinstance(tools_data, dict): + return None + tools: Dict[str, LDTool] = {} + for tool_name, tool_dict in tools_data.items(): + if isinstance(tool_dict, dict): + tools[tool_name] = LDTool( + name=str(tool_dict.get('name', tool_name)), + description=tool_dict.get('description'), + type=tool_dict.get('type'), + parameters=tool_dict.get('parameters'), + custom_parameters=tool_dict.get('customParameters'), + ) + return tools or None model = variation.get('model') if not isinstance(model, dict): @@ -84,13 +76,22 @@ def _resolve_tools(variation: Dict[str, Any]) -> Optional[Dict[str, LDTool]]: parameters = model.get('parameters') if not isinstance(parameters, dict): return None - tools_data = parameters.get('tools') - if not isinstance(tools_data, dict): - if tools_data is not None: - log.warning('Skipping model.parameters.tools: expected a dict, got %s', type(tools_data).__name__) + tools_list = parameters.get('tools') + if not isinstance(tools_list, list): return None - return _parse_tools(tools_data) + tools = {} + for item in tools_list: + if isinstance(item, dict) and item.get('name'): + tool_name = str(item['name']) + tools[tool_name] = LDTool( + name=tool_name, + description=item.get('description'), + type=item.get('type'), + parameters=item.get('parameters'), + custom_parameters=item.get('customParameters'), + ) + return tools or None class LDAIClient: diff --git a/packages/sdk/server-ai/tests/test_tools.py b/packages/sdk/server-ai/tests/test_tools.py index ba553a6..18b0d52 100644 --- a/packages/sdk/server-ai/tests/test_tools.py +++ b/packages/sdk/server-ai/tests/test_tools.py @@ -68,14 +68,14 @@ def td() -> TestData: 'name': 'gpt-5', 'parameters': { 'temperature': 0.5, - 'tools': { - 'param-tool': { + 'tools': [ + { 'name': 'param-tool', 'type': 'function', 'description': 'A tool from model params', 'parameters': {'type': 'object'}, } - }, + ], }, }, 'messages': [{'role': 'user', 'content': 'Hello'}], @@ -92,12 +92,12 @@ def td() -> TestData: 'model': { 'name': 'gpt-5', 'parameters': { - 'tools': { - 'model-param-tool': { + 'tools': [ + { 'name': 'model-param-tool', 'type': 'function', } - }, + ], }, }, 'messages': [{'role': 'user', 'content': 'Hello'}], @@ -114,7 +114,7 @@ def td() -> TestData: ) td.update( - td.flag('completion-model-params-tools-as-list') + td.flag('completion-model-params-tools-list-format') .variations( { 'model': { @@ -133,18 +133,17 @@ def td() -> TestData: ) td.update( - td.flag('completion-model-params-tools-missing-name') + td.flag('completion-model-params-tools-as-dict') .variations( { 'model': { 'name': 'gpt-5', 'parameters': { 'tools': { - 'valid-tool': { - 'name': 'valid-tool', + 'dict-tool': { + 'name': 'dict-tool', 'type': 'function', }, - 'bad-entry': 'not-a-dict', }, }, }, @@ -155,6 +154,29 @@ def td() -> TestData: .variation_for_all(0) ) + td.update( + td.flag('completion-model-params-tools-bad-entries') + .variations( + { + 'model': { + 'name': 'gpt-5', + 'parameters': { + 'tools': [ + { + 'name': 'valid-tool', + 'type': 'function', + }, + 'not-a-dict', + ], + }, + }, + 'messages': [{'role': 'user', 'content': 'Hello'}], + '_ldMeta': {'enabled': True, 'variationKey': 'v1', 'version': 1}, + }, + ) + .variation_for_all(0) + ) + return td @@ -249,15 +271,23 @@ def test_completion_config_root_tools_take_priority_over_model_params(client, co assert 'model-param-tool' not in result.tools -def test_completion_config_model_params_tools_as_list_returns_none(client, context): - result = client.completion_config('completion-model-params-tools-as-list', context, AICompletionConfigDefault()) +def test_completion_config_model_params_tools_list_format_is_parsed(client, context): + result = client.completion_config('completion-model-params-tools-list-format', context, AICompletionConfigDefault()) + + assert result.tools is not None + assert 'list-tool' in result.tools + assert result.tools['list-tool'].type == 'function' + + +def test_completion_config_model_params_tools_dict_format_returns_none(client, context): + result = client.completion_config('completion-model-params-tools-as-dict', context, AICompletionConfigDefault()) assert result.tools is None -def test_completion_config_model_params_tools_skips_bad_entries_silently(client, context): - result = client.completion_config('completion-model-params-tools-missing-name', context, AICompletionConfigDefault()) +def test_completion_config_model_params_tools_skips_bad_entries(client, context): + result = client.completion_config('completion-model-params-tools-bad-entries', context, AICompletionConfigDefault()) assert result.tools is not None assert 'valid-tool' in result.tools - assert 'bad-entry' not in result.tools + assert len(result.tools) == 1