|
17 | 17 | "model": "gpt-5.4", |
18 | 18 | "language": "en", |
19 | 19 | "pageindex_threshold": 20, |
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, |
27 | 20 | } |
28 | 21 |
|
29 | 22 | # Default entity-type vocabulary. Overridable per-KB via the optional |
@@ -151,36 +144,28 @@ def resolve_extra_headers(config: dict) -> dict[str, str]: |
151 | 144 | return headers |
152 | 145 |
|
153 | 146 |
|
154 | | -def resolve_parallel_tool_calls(config: dict) -> bool | None: |
155 | | - """Resolve the optional ``parallel_tool_calls:`` key. |
| 147 | +def resolve_parallel_tool_calls(config: dict) -> tuple[bool | None, bool]: |
| 148 | + """Resolve the optional ``parallel_tool_calls:`` key to ``(value, was_explicit)``. |
156 | 149 |
|
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. |
| 150 | + Absent → ``(None, False)`` so each agent applies its own default (see |
| 151 | + ``resolve_model_settings``). ``true``/``false`` → that bool; explicit |
| 152 | + ``null`` → ``None`` (omit — the Amazon Bedrock #175 escape hatch); both with |
| 153 | + ``was_explicit=True`` so they override every agent uniformly. An invalid |
| 154 | + value degrades to omit (never breaks a provider), with a warning. |
170 | 155 | """ |
171 | | - default = DEFAULT_CONFIG["parallel_tool_calls"] |
172 | | - value = config.get("parallel_tool_calls", default) |
| 156 | + if "parallel_tool_calls" not in config: |
| 157 | + return None, False |
| 158 | + value = config["parallel_tool_calls"] |
173 | 159 | if value is None: |
174 | | - return None |
175 | | - if not isinstance(value, bool): |
176 | | - logger.warning( |
177 | | - "config: 'parallel_tool_calls' must be true, false, or null, got %r " |
178 | | - "— using default (%r).", |
179 | | - value, |
180 | | - default, |
181 | | - ) |
182 | | - return default |
183 | | - return value |
| 160 | + return None, True |
| 161 | + if isinstance(value, bool): |
| 162 | + return value, True |
| 163 | + logger.warning( |
| 164 | + "config: 'parallel_tool_calls' must be true, false, or null, got %r " |
| 165 | + "— omitting the setting.", |
| 166 | + value, |
| 167 | + ) |
| 168 | + return None, True |
184 | 169 |
|
185 | 170 |
|
186 | 171 | def resolve_timeout(config: dict) -> float | None: |
@@ -282,38 +267,39 @@ def get_timeout_extra_args() -> dict[str, float] | None: |
282 | 267 | return {"timeout": _runtime_timeout} if _runtime_timeout is not None else None |
283 | 268 |
|
284 | 269 |
|
285 | | -# Process-wide agent ``parallel_tool_calls`` setting, resolved from config by |
286 | | -# the CLI (cli._setup_llm_key) and read when building query/chat agents. None = |
287 | | -# omit the setting (provider default) — see the DEFAULT_CONFIG note on why |
288 | | -# Bedrock needs this. |
289 | | -_runtime_parallel_tool_calls: bool | None = None |
| 270 | +# Process-wide agent ``parallel_tool_calls`` as ``(value, was_explicit)``, set |
| 271 | +# from config by the CLI and read when building agents. ``(None, False)`` = not |
| 272 | +# configured, so each agent falls back to its own default (resolve_model_settings). |
| 273 | +_runtime_parallel_tool_calls: tuple[bool | None, bool] = (None, False) |
290 | 274 |
|
291 | 275 |
|
292 | | -def set_parallel_tool_calls(value: bool | None) -> None: |
293 | | - """Set the process-wide agent ``parallel_tool_calls`` setting.""" |
| 276 | +def set_parallel_tool_calls(value: bool | None, was_explicit: bool) -> None: |
| 277 | + """Set the process-wide ``parallel_tool_calls`` — see :func:`resolve_parallel_tool_calls`.""" |
294 | 278 | global _runtime_parallel_tool_calls |
295 | | - _runtime_parallel_tool_calls = value |
| 279 | + _runtime_parallel_tool_calls = (value, was_explicit) |
296 | 280 |
|
297 | 281 |
|
298 | | -def get_parallel_tool_calls() -> bool | None: |
299 | | - """Return the process-wide agent ``parallel_tool_calls`` setting (or None).""" |
| 282 | +def get_parallel_tool_calls() -> tuple[bool | None, bool]: |
| 283 | + """Return the process-wide ``parallel_tool_calls`` as ``(value, was_explicit)``.""" |
300 | 284 | return _runtime_parallel_tool_calls |
301 | 285 |
|
302 | 286 |
|
303 | | -def resolve_model_settings() -> dict[str, Any]: |
| 287 | +def resolve_model_settings(*, default_parallel_tool_calls: bool | None = False) -> dict[str, Any]: |
304 | 288 | """Assemble the agents-SDK ``ModelSettings`` kwargs from the process-wide LLM |
305 | | - runtime settings (populated by ``cli._setup_llm_key``). |
| 289 | + runtime settings — the single place tool-using agent builders wire them in. |
306 | 290 |
|
307 | | - This is the single place that maps runtime LLM config onto agent model |
308 | | - settings: every agent builder does ``ModelSettings(**resolve_model_settings())`` |
309 | | - rather than enumerating the individual getters, so a new agent-facing knob |
310 | | - is wired in here once and can't be silently forgotten by one builder. |
311 | | - ``None`` values are what the agents SDK treats as "unset / provider default". |
| 291 | + ``default_parallel_tool_calls`` (the caller's own historical default) is used |
| 292 | + only when config didn't set ``parallel_tool_calls``; an explicit value always |
| 293 | + wins. Tool-less agents (skill-eval graders) skip this and omit the setting — |
| 294 | + the SDK forwards an explicit ``False`` even without tools, which strict |
| 295 | + OpenAI-compatible endpoints reject. |
312 | 296 | """ |
| 297 | + value, was_explicit = get_parallel_tool_calls() |
| 298 | + parallel_tool_calls = value if was_explicit else default_parallel_tool_calls |
313 | 299 | return { |
314 | 300 | "extra_headers": get_extra_headers() or None, |
315 | 301 | "extra_args": get_timeout_extra_args(), |
316 | | - "parallel_tool_calls": get_parallel_tool_calls(), |
| 302 | + "parallel_tool_calls": parallel_tool_calls, |
317 | 303 | } |
318 | 304 |
|
319 | 305 |
|
|
0 commit comments