|
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,48 @@ 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: |
| 147 | +def resolve_parallel_tool_calls(config: dict) -> tuple[bool | None, bool]: |
155 | 148 | """Resolve the optional ``parallel_tool_calls:`` key. |
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 | + Returns ``(value, was_explicit)``: |
| 151 | + * key absent → ``(None, False)`` — "not configured". Different agent |
| 152 | + builders had different HISTORICAL defaults before this setting existed |
| 153 | + (query/chat forced sequential; the semantic linter and skill-creation |
| 154 | + agents never forced a value at all), so there's no single systemwide |
| 155 | + default to fall back to here — ``resolve_model_settings()`` lets each |
| 156 | + caller supply its own, applied only when ``was_explicit`` is ``False``. |
| 157 | + * ``true`` / ``false`` → ``(that bool, True)`` — an explicit choice that |
| 158 | + overrides every agent's own default uniformly. |
| 159 | + * explicit ``null`` → ``(None, True)`` — "don't send the setting at all" |
| 160 | + (the provider decides). This is the escape hatch for Amazon Bedrock |
| 161 | + Claude, which rejects the request when the param is sent at all — |
| 162 | + distinct from "absent" even though both currently carry a value of |
| 163 | + ``None``; ``was_explicit`` is what tells them apart. |
| 164 | + * anything else (invalid) → ``(None, True)``, with a warning. Omitting |
| 165 | + is the one value known to never break any provider, so a malformed |
| 166 | + config value degrades to the safest behavior rather than to a fixed |
| 167 | + bool that could reproduce the exact failure (e.g. Bedrock's) the user |
| 168 | + may have set this key specifically to avoid. |
| 169 | +
|
| 170 | + This function does not depend on ``config`` being pre-merged with |
| 171 | + ``DEFAULT_CONFIG`` — ``parallel_tool_calls`` is intentionally absent from |
| 172 | + ``DEFAULT_CONFIG`` so ``load_config``'s merge can't mask "the user's |
| 173 | + config.yaml doesn't mention this key" as if it had been set to a value. |
170 | 174 | """ |
171 | | - default = DEFAULT_CONFIG["parallel_tool_calls"] |
172 | | - value = config.get("parallel_tool_calls", default) |
| 175 | + if "parallel_tool_calls" not in config: |
| 176 | + return None, False |
| 177 | + value = config["parallel_tool_calls"] |
173 | 178 | 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 |
| 179 | + return None, True |
| 180 | + if isinstance(value, bool): |
| 181 | + return value, True |
| 182 | + logger.warning( |
| 183 | + "config: 'parallel_tool_calls' must be true, false, or null, got %r " |
| 184 | + "— omitting the setting (the safest fallback; it never breaks a " |
| 185 | + "provider, unlike a fixed true/false might).", |
| 186 | + value, |
| 187 | + ) |
| 188 | + return None, True |
184 | 189 |
|
185 | 190 |
|
186 | 191 | def resolve_timeout(config: dict) -> float | None: |
@@ -283,37 +288,64 @@ def get_timeout_extra_args() -> dict[str, float] | None: |
283 | 288 |
|
284 | 289 |
|
285 | 290 | # 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 |
| 291 | +# the CLI (cli._setup_llm_key) and read when building agents. ``(None, False)`` |
| 292 | +# — the module's own raw default — means "not configured", matching what |
| 293 | +# ``resolve_parallel_tool_calls({})`` returns for an absent key, so an agent |
| 294 | +# built before ``_setup_llm_key`` has ever run defers to its own default via |
| 295 | +# ``resolve_model_settings()`` instead of landing on an arbitrary fixed value. |
| 296 | +_runtime_parallel_tool_calls: tuple[bool | None, bool] = (None, False) |
290 | 297 |
|
291 | 298 |
|
292 | | -def set_parallel_tool_calls(value: bool | None) -> None: |
293 | | - """Set the process-wide agent ``parallel_tool_calls`` setting.""" |
| 299 | +def set_parallel_tool_calls(value: bool | None, was_explicit: bool) -> None: |
| 300 | + """Set the process-wide agent ``parallel_tool_calls`` setting. |
| 301 | +
|
| 302 | + ``was_explicit`` distinguishes "the KB's config.yaml has no |
| 303 | + ``parallel_tool_calls`` key" (``False``) from "the user explicitly chose |
| 304 | + this value, including an explicit ``null``" (``True``) — see |
| 305 | + :func:`resolve_parallel_tool_calls`. |
| 306 | + """ |
294 | 307 | global _runtime_parallel_tool_calls |
295 | | - _runtime_parallel_tool_calls = value |
| 308 | + _runtime_parallel_tool_calls = (value, was_explicit) |
296 | 309 |
|
297 | 310 |
|
298 | | -def get_parallel_tool_calls() -> bool | None: |
299 | | - """Return the process-wide agent ``parallel_tool_calls`` setting (or None).""" |
| 311 | +def get_parallel_tool_calls() -> tuple[bool | None, bool]: |
| 312 | + """Return the process-wide agent ``parallel_tool_calls`` setting as |
| 313 | + ``(value, was_explicit)`` — see :func:`resolve_parallel_tool_calls`.""" |
300 | 314 | return _runtime_parallel_tool_calls |
301 | 315 |
|
302 | 316 |
|
303 | | -def resolve_model_settings() -> dict[str, Any]: |
| 317 | +def resolve_model_settings(*, default_parallel_tool_calls: bool | None = False) -> dict[str, Any]: |
304 | 318 | """Assemble the agents-SDK ``ModelSettings`` kwargs from the process-wide LLM |
305 | 319 | runtime settings (populated by ``cli._setup_llm_key``). |
306 | 320 |
|
307 | 321 | 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". |
| 322 | + settings: TOOL-USING agent builders (query, chat, lint, skill-create) do |
| 323 | + ``ModelSettings(**resolve_model_settings(...))`` rather than enumerating |
| 324 | + the individual getters, so a new agent-facing knob is wired in here once |
| 325 | + and can't be silently forgotten by one of them. ``None`` values are what |
| 326 | + the agents SDK treats as "unset / provider default". |
| 327 | +
|
| 328 | + Tool-LESS agents (e.g. the skill-eval graders) deliberately do NOT use |
| 329 | + this — ``parallel_tool_calls`` is meaningless without tools, and the |
| 330 | + agents SDK forwards an explicit ``False`` even to a tool-less request, |
| 331 | + which strict OpenAI-compatible endpoints reject. They build |
| 332 | + ``ModelSettings`` directly and omit ``parallel_tool_calls`` entirely. |
| 333 | +
|
| 334 | + ``default_parallel_tool_calls`` is applied ONLY when the KB's config.yaml |
| 335 | + doesn't configure ``parallel_tool_calls`` at all — pass your agent |
| 336 | + builder's own historical default here (``query``/``chat`` force |
| 337 | + sequential → ``False``, the module default; other builders pass their |
| 338 | + own). An explicit config value (``true``/``false``/``null``) always |
| 339 | + overrides every builder's default uniformly — that's what lets Amazon |
| 340 | + Bedrock users escape issue #175 with one setting, regardless of which |
| 341 | + agent they're running. |
312 | 342 | """ |
| 343 | + value, was_explicit = get_parallel_tool_calls() |
| 344 | + parallel_tool_calls = value if was_explicit else default_parallel_tool_calls |
313 | 345 | return { |
314 | 346 | "extra_headers": get_extra_headers() or None, |
315 | 347 | "extra_args": get_timeout_extra_args(), |
316 | | - "parallel_tool_calls": get_parallel_tool_calls(), |
| 348 | + "parallel_tool_calls": parallel_tool_calls, |
317 | 349 | } |
318 | 350 |
|
319 | 351 |
|
|
0 commit comments