Skip to content

Commit 2c30d75

Browse files
authored
fix: Fall back to model.parameters.tools when root tools absent (#146)
1 parent 3d5a6a9 commit 2c30d75

2 files changed

Lines changed: 152 additions & 3 deletions

File tree

packages/sdk/server-ai/src/ldai/client.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@
5656
def _parse_tools(tools_data: Optional[Dict[str, Any]]) -> Optional[Dict[str, LDTool]]:
5757
"""Parse the root-level tools map from a flag variation dict."""
5858
if not isinstance(tools_data, dict):
59+
if tools_data is not None:
60+
log.warning('Skipping tools: expected a dict, got %s', type(tools_data).__name__)
5961
return None
60-
result = {}
62+
result: Dict[str, LDTool] = {}
6163
for tool_name, tool_dict in tools_data.items():
6264
if not isinstance(tool_dict, dict):
6365
log.warning('Skipping tool "%s": expected a dict, got %s', tool_name, type(tool_dict).__name__)
@@ -72,6 +74,25 @@ def _parse_tools(tools_data: Optional[Dict[str, Any]]) -> Optional[Dict[str, LDT
7274
return result or None
7375

7476

77+
def _resolve_tools(variation: Dict[str, Any]) -> Optional[Dict[str, LDTool]]:
78+
if 'tools' in variation:
79+
return _parse_tools(variation['tools'])
80+
81+
model = variation.get('model')
82+
if not isinstance(model, dict):
83+
return None
84+
parameters = model.get('parameters')
85+
if not isinstance(parameters, dict):
86+
return None
87+
tools_data = parameters.get('tools')
88+
if not isinstance(tools_data, dict):
89+
if tools_data is not None:
90+
log.warning('Skipping model.parameters.tools: expected a dict, got %s', type(tools_data).__name__)
91+
return None
92+
93+
return _parse_tools(tools_data)
94+
95+
7596
class LDAIClient:
7697
"""The LaunchDarkly AI SDK client object."""
7798

@@ -117,7 +138,7 @@ def _completion_config(
117138
)
118139

119140
evaluator = self._build_evaluator(judge_configuration, context, default_ai_provider, variables)
120-
tools = _parse_tools(variation.get('tools'))
141+
tools = _resolve_tools(variation)
121142

122143
config = AICompletionConfig(
123144
key=key,
@@ -946,7 +967,7 @@ def __evaluate_agent(
946967
effective_judge_configuration = judge_configuration or JudgeConfiguration(judges=[])
947968

948969
evaluator = self._build_evaluator(effective_judge_configuration, context, default_ai_provider, variables)
949-
tools = _parse_tools(variation.get('tools'))
970+
tools = _resolve_tools(variation)
950971

951972
return AIAgentConfig(
952973
key=key,

packages/sdk/server-ai/tests/test_tools.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,101 @@ def td() -> TestData:
6060
.variation_for_all(0)
6161
)
6262

63+
td.update(
64+
td.flag('completion-tools-in-model-params')
65+
.variations(
66+
{
67+
'model': {
68+
'name': 'gpt-5',
69+
'parameters': {
70+
'temperature': 0.5,
71+
'tools': {
72+
'param-tool': {
73+
'name': 'param-tool',
74+
'type': 'function',
75+
'description': 'A tool from model params',
76+
'parameters': {'type': 'object'},
77+
}
78+
},
79+
},
80+
},
81+
'messages': [{'role': 'user', 'content': 'Hello'}],
82+
'_ldMeta': {'enabled': True, 'variationKey': 'v1', 'version': 1},
83+
},
84+
)
85+
.variation_for_all(0)
86+
)
87+
88+
td.update(
89+
td.flag('completion-root-and-model-params-tools')
90+
.variations(
91+
{
92+
'model': {
93+
'name': 'gpt-5',
94+
'parameters': {
95+
'tools': {
96+
'model-param-tool': {
97+
'name': 'model-param-tool',
98+
'type': 'function',
99+
}
100+
},
101+
},
102+
},
103+
'messages': [{'role': 'user', 'content': 'Hello'}],
104+
'tools': {
105+
'root-tool': {
106+
'name': 'root-tool',
107+
'type': 'function',
108+
}
109+
},
110+
'_ldMeta': {'enabled': True, 'variationKey': 'v1', 'version': 1},
111+
},
112+
)
113+
.variation_for_all(0)
114+
)
115+
116+
td.update(
117+
td.flag('completion-model-params-tools-as-list')
118+
.variations(
119+
{
120+
'model': {
121+
'name': 'gpt-5',
122+
'parameters': {
123+
'tools': [
124+
{'name': 'list-tool', 'type': 'function'},
125+
],
126+
},
127+
},
128+
'messages': [{'role': 'user', 'content': 'Hello'}],
129+
'_ldMeta': {'enabled': True, 'variationKey': 'v1', 'version': 1},
130+
},
131+
)
132+
.variation_for_all(0)
133+
)
134+
135+
td.update(
136+
td.flag('completion-model-params-tools-missing-name')
137+
.variations(
138+
{
139+
'model': {
140+
'name': 'gpt-5',
141+
'parameters': {
142+
'tools': {
143+
'valid-tool': {
144+
'name': 'valid-tool',
145+
'type': 'function',
146+
},
147+
'bad-entry': 'not-a-dict',
148+
},
149+
},
150+
},
151+
'messages': [{'role': 'user', 'content': 'Hello'}],
152+
'_ldMeta': {'enabled': True, 'variationKey': 'v1', 'version': 1},
153+
},
154+
)
155+
.variation_for_all(0)
156+
)
157+
63158
return td
64159

65160

@@ -133,3 +228,36 @@ def test_aitool_to_dict_omits_none_fields():
133228
d = tool.to_dict()
134229

135230
assert d == {'name': 'bare-tool'}
231+
232+
233+
def test_completion_config_tools_from_model_params_when_no_root_tools(client, context):
234+
result = client.completion_config('completion-tools-in-model-params', context, AICompletionConfigDefault())
235+
236+
assert result.tools is not None
237+
assert 'param-tool' in result.tools
238+
tool = result.tools['param-tool']
239+
assert tool.name == 'param-tool'
240+
assert tool.type == 'function'
241+
assert tool.description == 'A tool from model params'
242+
243+
244+
def test_completion_config_root_tools_take_priority_over_model_params(client, context):
245+
result = client.completion_config('completion-root-and-model-params-tools', context, AICompletionConfigDefault())
246+
247+
assert result.tools is not None
248+
assert 'root-tool' in result.tools
249+
assert 'model-param-tool' not in result.tools
250+
251+
252+
def test_completion_config_model_params_tools_as_list_returns_none(client, context):
253+
result = client.completion_config('completion-model-params-tools-as-list', context, AICompletionConfigDefault())
254+
255+
assert result.tools is None
256+
257+
258+
def test_completion_config_model_params_tools_skips_bad_entries_silently(client, context):
259+
result = client.completion_config('completion-model-params-tools-missing-name', context, AICompletionConfigDefault())
260+
261+
assert result.tools is not None
262+
assert 'valid-tool' in result.tools
263+
assert 'bad-entry' not in result.tools

0 commit comments

Comments
 (0)