Skip to content

Commit 7ce3db4

Browse files
jsonbaileyclaude
andcommitted
fix: parse model.parameters.tools as list, not dict
The wire format for tools at model.parameters.tools is a list of LDTool objects, while root-level tools is a dict keyed by tool name. Previously _resolve_tools treated both as dicts, causing the model params fallback path to always return None. Removes helper functions and inlines logic into _resolve_tools with no warnings, matching the JS implementation approach of trusting the data shape from the LD API. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 442f46a commit 7ce3db4

2 files changed

Lines changed: 74 additions & 45 deletions

File tree

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

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -53,44 +53,45 @@
5353
_DISABLED_JUDGE_DEFAULT = AIJudgeConfigDefault.disabled()
5454

5555

56-
def _parse_tools(tools_data: Optional[Dict[str, Any]]) -> Optional[Dict[str, LDTool]]:
57-
"""Parse the root-level tools map from a flag variation dict."""
58-
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__)
61-
return None
62-
result: Dict[str, LDTool] = {}
63-
for tool_name, tool_dict in tools_data.items():
64-
if not isinstance(tool_dict, dict):
65-
log.warning('Skipping tool "%s": expected a dict, got %s', tool_name, type(tool_dict).__name__)
66-
continue
67-
result[tool_name] = LDTool(
68-
name=tool_dict.get('name', tool_name),
69-
description=tool_dict.get('description'),
70-
type=tool_dict.get('type'),
71-
parameters=tool_dict.get('parameters'),
72-
custom_parameters=tool_dict.get('customParameters'),
73-
)
74-
return result or None
75-
76-
7756
def _resolve_tools(variation: Dict[str, Any]) -> Optional[Dict[str, LDTool]]:
7857
if 'tools' in variation:
79-
return _parse_tools(variation['tools'])
58+
tools_data = variation['tools']
59+
if not isinstance(tools_data, dict):
60+
return None
61+
tools: Dict[str, LDTool] = {}
62+
for tool_name, tool_dict in tools_data.items():
63+
if isinstance(tool_dict, dict):
64+
tools[tool_name] = LDTool(
65+
name=str(tool_dict.get('name', tool_name)),
66+
description=tool_dict.get('description'),
67+
type=tool_dict.get('type'),
68+
parameters=tool_dict.get('parameters'),
69+
custom_parameters=tool_dict.get('customParameters'),
70+
)
71+
return tools or None
8072

8173
model = variation.get('model')
8274
if not isinstance(model, dict):
8375
return None
8476
parameters = model.get('parameters')
8577
if not isinstance(parameters, dict):
8678
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__)
79+
tools_list = parameters.get('tools')
80+
if not isinstance(tools_list, list):
9181
return None
9282

93-
return _parse_tools(tools_data)
83+
tools = {}
84+
for item in tools_list:
85+
if isinstance(item, dict) and item.get('name'):
86+
tool_name = str(item['name'])
87+
tools[tool_name] = LDTool(
88+
name=tool_name,
89+
description=item.get('description'),
90+
type=item.get('type'),
91+
parameters=item.get('parameters'),
92+
custom_parameters=item.get('customParameters'),
93+
)
94+
return tools or None
9495

9596

9697
class LDAIClient:

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

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,14 @@ def td() -> TestData:
6868
'name': 'gpt-5',
6969
'parameters': {
7070
'temperature': 0.5,
71-
'tools': {
72-
'param-tool': {
71+
'tools': [
72+
{
7373
'name': 'param-tool',
7474
'type': 'function',
7575
'description': 'A tool from model params',
7676
'parameters': {'type': 'object'},
7777
}
78-
},
78+
],
7979
},
8080
},
8181
'messages': [{'role': 'user', 'content': 'Hello'}],
@@ -92,12 +92,12 @@ def td() -> TestData:
9292
'model': {
9393
'name': 'gpt-5',
9494
'parameters': {
95-
'tools': {
96-
'model-param-tool': {
95+
'tools': [
96+
{
9797
'name': 'model-param-tool',
9898
'type': 'function',
9999
}
100-
},
100+
],
101101
},
102102
},
103103
'messages': [{'role': 'user', 'content': 'Hello'}],
@@ -114,7 +114,7 @@ def td() -> TestData:
114114
)
115115

116116
td.update(
117-
td.flag('completion-model-params-tools-as-list')
117+
td.flag('completion-model-params-tools-list-format')
118118
.variations(
119119
{
120120
'model': {
@@ -133,18 +133,17 @@ def td() -> TestData:
133133
)
134134

135135
td.update(
136-
td.flag('completion-model-params-tools-missing-name')
136+
td.flag('completion-model-params-tools-as-dict')
137137
.variations(
138138
{
139139
'model': {
140140
'name': 'gpt-5',
141141
'parameters': {
142142
'tools': {
143-
'valid-tool': {
144-
'name': 'valid-tool',
143+
'dict-tool': {
144+
'name': 'dict-tool',
145145
'type': 'function',
146146
},
147-
'bad-entry': 'not-a-dict',
148147
},
149148
},
150149
},
@@ -155,6 +154,29 @@ def td() -> TestData:
155154
.variation_for_all(0)
156155
)
157156

157+
td.update(
158+
td.flag('completion-model-params-tools-bad-entries')
159+
.variations(
160+
{
161+
'model': {
162+
'name': 'gpt-5',
163+
'parameters': {
164+
'tools': [
165+
{
166+
'name': 'valid-tool',
167+
'type': 'function',
168+
},
169+
'not-a-dict',
170+
],
171+
},
172+
},
173+
'messages': [{'role': 'user', 'content': 'Hello'}],
174+
'_ldMeta': {'enabled': True, 'variationKey': 'v1', 'version': 1},
175+
},
176+
)
177+
.variation_for_all(0)
178+
)
179+
158180
return td
159181

160182

@@ -249,17 +271,23 @@ def test_completion_config_root_tools_take_priority_over_model_params(client, co
249271
assert 'model-param-tool' not in result.tools
250272

251273

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())
274+
def test_completion_config_model_params_tools_list_format_is_parsed(client, context):
275+
result = client.completion_config('completion-model-params-tools-list-format', context, AICompletionConfigDefault())
276+
277+
assert result.tools is not None
278+
assert 'list-tool' in result.tools
279+
assert result.tools['list-tool'].type == 'function'
280+
281+
282+
def test_completion_config_model_params_tools_dict_format_returns_none(client, context):
283+
result = client.completion_config('completion-model-params-tools-as-dict', context, AICompletionConfigDefault())
254284

255285
assert result.tools is None
256286

257287

258-
def test_completion_config_model_params_tools_skips_bad_entries_silently(client, context):
259-
result = client.completion_config(
260-
'completion-model-params-tools-missing-name', context, AICompletionConfigDefault()
261-
)
288+
def test_completion_config_model_params_tools_skips_bad_entries(client, context):
289+
result = client.completion_config('completion-model-params-tools-bad-entries', context, AICompletionConfigDefault())
262290

263291
assert result.tools is not None
264292
assert 'valid-tool' in result.tools
265-
assert 'bad-entry' not in result.tools
293+
assert len(result.tools) == 1

0 commit comments

Comments
 (0)