From 9ee71cda9eacf19006d4c0c85d0d40ff8d40f02a Mon Sep 17 00:00:00 2001 From: chinesepowered <22500229+chinesepowered@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:55:38 -0700 Subject: [PATCH 1/2] fix(sdk-python): validate max_tool_calls and max_subagent_depth as integers max_session_turns rejects bools and non-ints before its range check, but the two sibling limits only range-checked, so values their own error messages rule out were accepted and stringified onto the CLI: max_tool_calls=True -> --max-tool-calls True max_tool_calls=2.5 -> --max-tool-calls 2.5 max_subagent_depth=True -> --max-subagent-depth True bool is the sharp edge: it subclasses int, so True passes both isinstance and the range comparison as 1. Extract the bool-aware check the max_session_turns branch already performed into _is_int and apply it to all three. max_session_turns keeps its exact previous behavior; the expression is the same, only named. --- .../src/qwen_code_sdk/validation.py | 22 +++++++++++----- .../sdk-python/tests/unit/test_validation.py | 25 +++++++++++++++++++ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/packages/sdk-python/src/qwen_code_sdk/validation.py b/packages/sdk-python/src/qwen_code_sdk/validation.py index 5e144da4d1e..03492cd0491 100644 --- a/packages/sdk-python/src/qwen_code_sdk/validation.py +++ b/packages/sdk-python/src/qwen_code_sdk/validation.py @@ -135,9 +135,7 @@ def validate_query_options(options: QueryOptions) -> None: ) if options.max_session_turns is not None and ( - isinstance(options.max_session_turns, bool) - or not isinstance(options.max_session_turns, int) - or options.max_session_turns < -1 + not _is_int(options.max_session_turns) or options.max_session_turns < -1 ): raise ValidationError("max_session_turns must be -1 or a non-negative integer") @@ -147,11 +145,14 @@ def validate_query_options(options: QueryOptions) -> None: ): raise ValidationError("path_to_qwen_executable cannot be empty") - if options.max_tool_calls is not None and options.max_tool_calls < -1: + if options.max_tool_calls is not None and ( + not _is_int(options.max_tool_calls) or options.max_tool_calls < -1 + ): raise ValidationError("max_tool_calls must be -1 or a non-negative integer") - if options.max_subagent_depth is not None and not ( - 1 <= options.max_subagent_depth <= 100 + if options.max_subagent_depth is not None and ( + not _is_int(options.max_subagent_depth) + or not (1 <= options.max_subagent_depth <= 100) ): raise ValidationError("max_subagent_depth must be between 1 and 100") @@ -205,6 +206,15 @@ def validate_query_options(options: QueryOptions) -> None: raise ValidationError("proxy cannot be empty") +def _is_int(value: object) -> bool: + """True for a real integer. + + ``bool`` subclasses ``int``, so ``isinstance(True, int)`` is True and a + bare isinstance check would let ``max_tool_calls=True`` through as 1. + """ + return isinstance(value, int) and not isinstance(value, bool) + + def _validate_optional_callable( value: object, validator: Callable[[object, type[ValidationError]], None], diff --git a/packages/sdk-python/tests/unit/test_validation.py b/packages/sdk-python/tests/unit/test_validation.py index 10bdfd1d05c..25ad4cf65fa 100644 --- a/packages/sdk-python/tests/unit/test_validation.py +++ b/packages/sdk-python/tests/unit/test_validation.py @@ -188,6 +188,31 @@ def test_rejects_invalid_max_subagent_depth() -> None: validate_query_options(QueryOptions(max_subagent_depth=0)) +@pytest.mark.parametrize("value", [True, False, 0.5]) +def test_rejects_non_integer_max_tool_calls(value: object) -> None: + # The error message promises "-1 or a non-negative integer", and the value + # is stringified straight onto the CLI, so `True` would become + # `--max-tool-calls True`. + with pytest.raises(ValidationError, match="max_tool_calls"): + validate_query_options(QueryOptions(max_tool_calls=cast(Any, value))) + + +@pytest.mark.parametrize("value", [True, False, 2.5]) +def test_rejects_non_integer_max_subagent_depth(value: object) -> None: + with pytest.raises(ValidationError, match="max_subagent_depth"): + validate_query_options(QueryOptions(max_subagent_depth=cast(Any, value))) + + +def test_accepts_valid_integer_limits() -> None: + # The in-range integers these options are documented to take must survive. + validate_query_options( + QueryOptions(max_tool_calls=-1, max_session_turns=-1, max_subagent_depth=1) + ) + validate_query_options( + QueryOptions(max_tool_calls=0, max_session_turns=0, max_subagent_depth=100) + ) + + def test_rejects_agents_missing_required_fields() -> None: with pytest.raises(ValidationError, match="missing required field"): validate_query_options(QueryOptions(agents=[{"name": "test"}])) From 8265558b86074b565578ee3eccb5ade56d5b82ad Mon Sep 17 00:00:00 2001 From: chinesepowered <22500229+chinesepowered@users.noreply.github.com> Date: Wed, 22 Jul 2026 19:14:51 -0700 Subject: [PATCH 2/2] fix(sdk-python): say integer in the max_subagent_depth range message A float like 2.5 is between 1 and 100, so the old message described a constraint the value already satisfied and gave no hint that the type was the problem. --- packages/sdk-python/src/qwen_code_sdk/validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk-python/src/qwen_code_sdk/validation.py b/packages/sdk-python/src/qwen_code_sdk/validation.py index 03492cd0491..d52dc9c7942 100644 --- a/packages/sdk-python/src/qwen_code_sdk/validation.py +++ b/packages/sdk-python/src/qwen_code_sdk/validation.py @@ -154,7 +154,7 @@ def validate_query_options(options: QueryOptions) -> None: not _is_int(options.max_subagent_depth) or not (1 <= options.max_subagent_depth <= 100) ): - raise ValidationError("max_subagent_depth must be between 1 and 100") + raise ValidationError("max_subagent_depth must be an integer between 1 and 100") if options.agents: for i, agent in enumerate(options.agents):