Skip to content

Commit 2c30d18

Browse files
committed
refactor(config): default parallel_tool_calls to false; null = omit for Bedrock
Revised per review: the default must NOT change existing behavior. Keep the historical default (false = force sequential tool calls) for every provider, and make `null` the explicit "don't send the setting" escape hatch that Bedrock Claude needs (sending any value trips the LiteLLM malformed-tool_choice bug, #175). No provider special-casing in code — it's purely the config value. - DEFAULT_CONFIG parallel_tool_calls: false (was None). - resolve_parallel_tool_calls: absent → false (default); true/false → that; explicit null → None (omit, silent); other → default + warning. - Docs updated: default false, set `null` for Bedrock.
1 parent ea8db23 commit 2c30d18

6 files changed

Lines changed: 74 additions & 43 deletions

File tree

config.yaml.example

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ model: gpt-5.4 # LLM model (any LiteLLM-supported provider)
22
language: en # Wiki output language
33
pageindex_threshold: 20 # PDF pages threshold for PageIndex
44

5-
# Optional: whether query/chat agents may call tools in parallel. Omit (the
6-
# default) to let the provider decide. Leave it unset for Amazon Bedrock Claude
7-
# models — sending it makes LiteLLM emit a malformed tool_choice and every
8-
# query/chat fails. Set false to force sequential tool calls on other providers.
9-
# parallel_tool_calls: false
5+
# Optional: whether query/chat agents may call tools in parallel.
6+
# false (default) force sequential tool calls
7+
# true allow parallel tool calls
8+
# null don't send the setting (use the provider default) — set this
9+
# for Amazon Bedrock Claude, which rejects the request when
10+
# parallel_tool_calls is sent at all (any value). See #175.
11+
# parallel_tool_calls: null
1012

1113
# Optional: override the entity-type vocabulary used for entity pages.
1214
# Omit this key to use the default 7 types

examples/configuration/README.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,13 @@ model: gpt-5.4 # LLM model (any LiteLLM-supported provider)
7070
language: en # Wiki output language
7171
pageindex_threshold: 20 # PDF pages threshold for PageIndex
7272

73-
# Optional: whether query/chat agents may call tools in parallel. Omit (the
74-
# default) to let the provider decide. Leave it unset for Amazon Bedrock Claude
75-
# models — sending it makes LiteLLM emit a malformed tool_choice and every
76-
# query/chat fails. Set false to force sequential tool calls on other providers.
77-
# parallel_tool_calls: false
73+
# Optional: whether query/chat agents may call tools in parallel.
74+
# false (default) force sequential tool calls
75+
# true allow parallel tool calls
76+
# null don't send the setting (use the provider default) — set this
77+
# for Amazon Bedrock Claude, which rejects the request when
78+
# parallel_tool_calls is sent at all (any value). See #175.
79+
# parallel_tool_calls: null
7880

7981
# Optional: override the entity-type vocabulary used for entity pages.
8082
# Omit this key to use the default 7 types
@@ -101,7 +103,7 @@ pageindex_threshold: 20 # PDF pages threshold for PageIndex
101103
| `model` | `gpt-5.4` | LLM used for all compile/query/chat work. |
102104
| `language` | `en` | Language the wiki is written in. |
103105
| `pageindex_threshold` | `20` | PDFs with this many pages **or more** take the long-doc (PageIndex) path; shorter ones go through the short-doc path. See [`pageindex-cloud/`](../pageindex-cloud/). |
104-
| `parallel_tool_calls` | *(unset)* | Whether query/chat agents may call tools in parallel. Unset = provider default. **Leave unset for Amazon Bedrock** (see below). Set `false` to force sequential tool calls elsewhere. |
106+
| `parallel_tool_calls` | `false` | Whether query/chat agents may call tools in parallel. `false` (default) forces sequential; `true` allows parallel; `null` omits the setting (provider default). **Amazon Bedrock needs `null`** (see below). |
105107
| `entity_types` | 7 defaults | Custom vocabulary for entity pages. `other` is always kept. |
106108
| `litellm:` || A pass-through block for LiteLLM. See below. |
107109

@@ -198,9 +200,10 @@ LLM_API_KEY=your-key-here
198200
```yaml
199201
# <kb>/.openkb/config.yaml
200202
model: bedrock/eu.anthropic.claude-sonnet-4-6
201-
# Do NOT set parallel_tool_calls for Bedrock Claude — leaving it unset (the
202-
# default) is what keeps query/chat working; setting it makes LiteLLM send a
203-
# malformed tool_choice that Bedrock rejects (issue #175).
203+
parallel_tool_calls: null # REQUIRED for Bedrock Claude: the default (false)
204+
# — and any explicit value — makes LiteLLM send a
205+
# malformed tool_choice that Bedrock rejects (#175).
206+
# null tells OpenKB not to send the setting at all.
204207
```
205208

206209
**Where keys are read from** (first match wins, existing env always respected):

openkb/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ def _setup_llm_key(kb_dir: Path | None = None) -> None:
176176
provider: str | None = None
177177
extra_headers: dict[str, str] = {}
178178
timeout: float | None = None
179-
parallel_tool_calls: bool | None = None
179+
parallel_tool_calls: bool | None = DEFAULT_CONFIG["parallel_tool_calls"]
180180
litellm_settings: dict = {}
181181
if kb_dir is not None:
182182
config_path = kb_dir / ".openkb" / "config.yaml"

openkb/config.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717
"model": "gpt-5.4",
1818
"language": "en",
1919
"pageindex_threshold": 20,
20-
# Whether query/chat agents may call tools in parallel. None (default) omits
21-
# the setting so the provider decides — required for Amazon Bedrock's Claude
22-
# models, where sending parallel_tool_calls makes LiteLLM emit a malformed
23-
# tool_choice (missing `type`) and every query/chat fails (issue #175). Set
24-
# false to force sequential tool calls (fine on OpenAI/Anthropic-direct).
25-
"parallel_tool_calls": None,
20+
# Whether query/chat agents may call tools in parallel. Default false =
21+
# force sequential tool calls (historical behavior). true = allow parallel.
22+
# null = don't send the setting at all (use the provider default) — the
23+
# escape hatch for Amazon Bedrock Claude, where sending parallel_tool_calls
24+
# (any value) makes LiteLLM emit a malformed tool_choice missing `type`, so
25+
# every query/chat fails (issue #175).
26+
"parallel_tool_calls": False,
2627
}
2728

2829
# Default entity-type vocabulary. Overridable per-KB via the optional
@@ -153,20 +154,32 @@ def resolve_extra_headers(config: dict) -> dict[str, str]:
153154
def resolve_parallel_tool_calls(config: dict) -> bool | None:
154155
"""Resolve the optional ``parallel_tool_calls:`` key.
155156
156-
Returns ``None`` (omit the setting — provider default) when absent or
157-
explicitly null; ``True``/``False`` when set to a bool. A non-bool value is
158-
invalid and treated as unset, with a warning. ``None`` is the normal
159-
"unset" case and warns silently, matching ``resolve_timeout``.
157+
Tri-state:
158+
* key absent → the default (``False`` — force sequential tool calls).
159+
* ``true`` / ``false`` → that bool.
160+
* explicit ``null`` → ``None``, meaning "don't send the setting" so the
161+
provider's own default applies. This is the escape hatch for Amazon
162+
Bedrock Claude, which rejects the request when the param is sent at all.
163+
164+
A non-bool, non-null value is invalid → falls back to the default with a
165+
warning. An explicit ``null`` is a valid choice and warns silently.
166+
167+
Note this relies on the config being merged with ``DEFAULT_CONFIG`` (as
168+
``load_config`` does), so an omitted key reads back as ``False`` while an
169+
explicit ``null`` reads back as ``None`` — the two are distinguishable.
160170
"""
161-
value = config.get("parallel_tool_calls")
171+
default = DEFAULT_CONFIG["parallel_tool_calls"]
172+
value = config.get("parallel_tool_calls", default)
162173
if value is None:
163174
return None
164175
if not isinstance(value, bool):
165176
logger.warning(
166-
"config: 'parallel_tool_calls' must be true or false, got %r — ignoring it.",
177+
"config: 'parallel_tool_calls' must be true, false, or null, got %r "
178+
"— using default (%r).",
167179
value,
180+
default,
168181
)
169-
return None
182+
return default
170183
return value
171184

172185

tests/test_config.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,33 @@
1717
)
1818

1919

20-
def test_parallel_tool_calls_defaults_to_none():
21-
assert DEFAULT_CONFIG["parallel_tool_calls"] is None
20+
def test_parallel_tool_calls_defaults_to_false():
21+
# Default preserves the historical behavior: force sequential tool calls.
22+
assert DEFAULT_CONFIG["parallel_tool_calls"] is False
2223

2324

24-
def test_resolve_parallel_tool_calls_absent_is_none():
25-
assert resolve_parallel_tool_calls({}) is None
25+
def test_resolve_parallel_tool_calls_absent_is_false():
26+
# Key omitted → the default (force sequential), same as before this knob existed.
27+
assert resolve_parallel_tool_calls({}) is False
2628

2729

2830
def test_resolve_parallel_tool_calls_explicit_bools():
2931
assert resolve_parallel_tool_calls({"parallel_tool_calls": True}) is True
3032
assert resolve_parallel_tool_calls({"parallel_tool_calls": False}) is False
3133

3234

33-
def test_resolve_parallel_tool_calls_none_is_silent(caplog):
35+
def test_resolve_parallel_tool_calls_null_means_omit(caplog):
36+
# Explicit null = "don't send the param" (provider default). This is the
37+
# escape hatch for Amazon Bedrock, and is silent (not an invalid value).
3438
with caplog.at_level(logging.WARNING, logger="openkb.config"):
3539
assert resolve_parallel_tool_calls({"parallel_tool_calls": None}) is None
3640
assert caplog.text == ""
3741

3842

3943
def test_resolve_parallel_tool_calls_rejects_non_bool(caplog):
40-
# A non-bool (e.g. a string or int) is invalid → treat as unset, with a warning.
44+
# A non-bool, non-null value is invalid → fall back to the default, with a warning.
4145
with caplog.at_level(logging.WARNING, logger="openkb.config"):
42-
assert resolve_parallel_tool_calls({"parallel_tool_calls": "true"}) is None
46+
assert resolve_parallel_tool_calls({"parallel_tool_calls": "true"}) is False
4347
assert "parallel_tool_calls" in caplog.text
4448

4549

tests/test_query.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -161,26 +161,35 @@ def test_no_extra_headers_by_default(self, tmp_path):
161161

162162

163163
class TestQueryAgentParallelToolCalls:
164-
"""Config-driven parallel_tool_calls reaches the agents-SDK model settings.
164+
"""The resolved parallel_tool_calls value (stash) reaches the model settings.
165165
166-
Default is None (omit the param → provider default), which is what keeps
167-
Bedrock's Claude models working: passing parallel_tool_calls at all makes
168-
LiteLLM emit a tool_choice object missing the required `type` field (see
169-
issue #175). Users can still force sequential tool calls with
170-
`parallel_tool_calls: false`.
166+
Default is False (force sequential — unchanged historical behavior). A
167+
config value of null resolves to None, which the agents-SDK omits from the
168+
request — the escape hatch for Amazon Bedrock, whose Claude models reject
169+
the request when parallel_tool_calls is sent at all (issue #175).
171170
"""
172171

173-
def test_omitted_by_default(self, tmp_path):
172+
def test_null_stash_is_omitted(self, tmp_path):
173+
from openkb.config import set_parallel_tool_calls
174+
175+
set_parallel_tool_calls(None)
174176
agent = build_query_agent(str(tmp_path), "bedrock/eu.anthropic.claude-sonnet-4-6")
175177
assert agent.model_settings.parallel_tool_calls is None
176178

177-
def test_applied_from_stash(self, tmp_path):
179+
def test_false_stash_forces_sequential(self, tmp_path):
178180
from openkb.config import set_parallel_tool_calls
179181

180182
set_parallel_tool_calls(False)
181183
agent = build_query_agent(str(tmp_path), "gpt-4o-mini")
182184
assert agent.model_settings.parallel_tool_calls is False
183185

186+
def test_true_stash_allows_parallel(self, tmp_path):
187+
from openkb.config import set_parallel_tool_calls
188+
189+
set_parallel_tool_calls(True)
190+
agent = build_query_agent(str(tmp_path), "gpt-4o-mini")
191+
assert agent.model_settings.parallel_tool_calls is True
192+
184193

185194
class TestQueryAgentTimeout:
186195
"""Config-driven timeout reaches the agents-SDK model settings via extra_args.

0 commit comments

Comments
 (0)