Skip to content

Commit 838b5b2

Browse files
jsonbaileyclaude
andcommitted
feat: Fall back to model.parameters.tools when root tools key is absent
When a flag variation has no root-level `tools` key, resolve tools from `variation['model']['parameters']['tools']` instead. Root-level presence always takes priority; the model-params fallback is entirely silent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3d5a6a9 commit 838b5b2

2 files changed

Lines changed: 158 additions & 2 deletions

File tree

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

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,34 @@ def _parse_tools(tools_data: Optional[Dict[str, Any]]) -> Optional[Dict[str, LDT
7272
return result or None
7373

7474

75+
def _resolve_tools(variation: Dict[str, Any]) -> Optional[Dict[str, LDTool]]:
76+
if 'tools' in variation:
77+
return _parse_tools(variation['tools'])
78+
79+
model = variation.get('model')
80+
if not isinstance(model, dict):
81+
return None
82+
parameters = model.get('parameters')
83+
if not isinstance(parameters, dict):
84+
return None
85+
tools_data = parameters.get('tools')
86+
if not isinstance(tools_data, dict):
87+
return None
88+
89+
result = {}
90+
for tool_name, tool_dict in tools_data.items():
91+
if not isinstance(tool_dict, dict):
92+
continue
93+
result[tool_name] = LDTool(
94+
name=tool_dict.get('name', tool_name),
95+
description=tool_dict.get('description'),
96+
type=tool_dict.get('type'),
97+
parameters=tool_dict.get('parameters'),
98+
custom_parameters=tool_dict.get('customParameters'),
99+
)
100+
return result or None
101+
102+
75103
class LDAIClient:
76104
"""The LaunchDarkly AI SDK client object."""
77105

@@ -117,7 +145,7 @@ def _completion_config(
117145
)
118146

119147
evaluator = self._build_evaluator(judge_configuration, context, default_ai_provider, variables)
120-
tools = _parse_tools(variation.get('tools'))
148+
tools = _resolve_tools(variation)
121149

122150
config = AICompletionConfig(
123151
key=key,
@@ -946,7 +974,7 @@ def __evaluate_agent(
946974
effective_judge_configuration = judge_configuration or JudgeConfiguration(judges=[])
947975

948976
evaluator = self._build_evaluator(effective_judge_configuration, context, default_ai_provider, variables)
949-
tools = _parse_tools(variation.get('tools'))
977+
tools = _resolve_tools(variation)
950978

951979
return AIAgentConfig(
952980
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)