Skip to content

feat(sdk): add reasoning effort option to CLI and both SDKs - #6464

Closed
juhuan wants to merge 1 commit into
QwenLM:mainfrom
juhuan:feat/sdk-effort
Closed

feat(sdk): add reasoning effort option to CLI and both SDKs#6464
juhuan wants to merge 1 commit into
QwenLM:mainfrom
juhuan:feat/sdk-effort

Conversation

@juhuan

@juhuan juhuan commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add set_effort control request to CLI's SystemController, with routing in ControlDispatcher
  • Accept effort in the initialize payload so it can be set at startup
  • Report can_set_effort capability in the initialize response
  • Python SDK: effort field in QueryOptions (with validation), Effort type alias, set_effort() runtime method, CLIControlSetEffortRequest protocol type
  • TypeScript SDK: effort field in QueryOptions/TransportOptions (with Zod schema), setEffort() runtime method, SET_EFFORT in ControlRequestType enum, CLIControlSetEffortRequest protocol interface

Valid effort values: low, medium, high, xhigh, max.

Test plan

  • Python SDK tests pass (pytest — 58 passed)
  • TypeScript SDK typecheck passes (tsc --noEmit)
  • TypeScript SDK tests pass (vitest run — 1163 passed)
  • Manual: verify effort is applied when set via QueryOptions
  • Manual: verify set_effort() / setEffort() changes effort at runtime

Add support for controlling the model's reasoning effort level
(low/medium/high/xhigh/max) via:

- CLI: handle `set_effort` control request, accept `effort` in
  initialize payload, report `can_set_effort` capability
- Python SDK: `effort` option in QueryOptions, `set_effort()` method
- TypeScript SDK: `effort` option in QueryOptions/TransportOptions,
  `setEffort()` method, `SET_EFFORT` control request type

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @juhuan — thanks for the PR! The feature itself (reasoning effort control via SDK) is a reasonable addition and the direction aligns with existing set_model / setModel patterns. But the PR body doesn't follow the required template.

Template: the body uses ## Summary and ## Test plan, but the PR template requires specific sections: ## What this PR does, ## Why it's needed, ## Reviewer Test Plan (with How to verify, Evidence (Before & After), Tested on matrix), ## Risk & Scope, ## Linked Issues, and a Chinese <details> translation block. Please restructure the body to match.

A few things I noticed while skimming the diff — worth addressing alongside the template:

  • Protocol type weakness: effort is typed as string in CLIControlSetEffortRequest, CLIControlInitializeRequest, and the Python protocol.py TypedDict. The core already has ReasoningEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max' — the protocol types should use the constrained type (or a Literal in Python) rather than a bare string, so invalid values fail at the type boundary, not at runtime.
  • Silent failure in initialize: when setReasoningEffort throws during handleInitialize, the error is caught and logged but the initialize response still succeeds. This means a bad effort value at startup is silently ignored. The runtime handleSetEffort correctly throws. Consider whether the initialize path should also propagate the error (or at minimum validate and reject before calling setReasoningEffort).
  • No tests in the diff: the PR body claims 58 Python tests passed and 1163 TypeScript tests passed, but no test files appear in the diff. Were new tests added for set_effort / setEffort, or were these just existing suites confirming no regressions? If no new tests, consider adding at least a validation test and a control-request round-trip test.

Please update the body to follow the template and address the type/validation notes. Happy to re-run the review after that.

中文说明

@juhuan 你好,感谢提交 PR!功能本身(通过 SDK 控制 reasoning effort)是合理的扩展,方向也与现有的 set_model / setModel 模式一致。但 PR 正文未遵循模板要求。

模板:正文使用了 ## Summary## Test plan,但 PR 模板 要求特定的章节:## What this PR does## Why it's needed## Reviewer Test Plan(含 How to verifyEvidence (Before & After)Tested on 矩阵)、## Risk & Scope## Linked Issues,以及中文 <details> 翻译块。请按模板重构正文。

浏览 diff 时发现几个值得一起修改的问题:

  • 协议类型偏弱CLIControlSetEffortRequestCLIControlInitializeRequest 和 Python protocol.py TypedDict 中 effort 的类型为 string。核心代码已有 ReasoningEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max'——协议类型应使用约束类型(Python 中用 Literal),而非裸 string,这样无效值在类型层面就会报错,而非等到运行时。
  • 初始化时静默失败handleInitializesetReasoningEffort 抛异常时,错误被捕获并记录,但 initialize 响应仍然成功。这意味着启动时的无效 effort 值被静默忽略。运行时 handleSetEffort 正确地抛出异常。请考虑 initialize 路径是否也应传播错误(或至少在调用前做校验并拒绝)。
  • diff 中没有测试文件:PR 正文声称 58 个 Python 测试通过、1163 个 TypeScript 测试通过,但 diff 中未出现测试文件。是否有为 set_effort / setEffort 新增测试,还是仅跑了已有测试确认无回归?如果没有新测试,建议至少添加一个验证测试和一个 control-request 往返测试。

请按模板更新正文并处理上述类型/验证问题。更新后可以重新触发审查。

Qwen Code · qwen3.7-max

@yiliang114

Copy link
Copy Markdown
Collaborator

Hey @juhuan, thanks for working on SDK completeness — these are all useful additions.

I noticed you've opened 5 separate PRs today (#6458, #6460, #6461, #6463, #6464), each exposing a single CLI feature to the SDKs. Looking at the diffs, the total changes across all 5 PRs are around 400 lines, and most of them touch overlapping file sets (e.g., transport.py, types.py, validation.py, createQuery.ts, ProcessTransport.ts for the query options; systemController.ts + ControlDispatcher.ts for the control requests).

Would it make more sense to consolidate these into a single PR? A few reasons:

  1. Lower review overhead — reviewers can evaluate the full SDK surface expansion in one pass rather than context-switching across 5 PRs.
  2. Consistent API surface — shipping all new SDK options together makes it easier to document and release as a coherent feature set.
  3. Avoid unnecessary merge conflictsfeat(sdk): add get_available_models() to Python and TypeScript SDKs #6460 and feat(sdk): add reasoning effort option to CLI and both SDKs #6464 both modify systemController.ts, which means they'll conflict with each other regardless of merge order.

If there's a specific reason to keep them separate (e.g., different reviewers, staged rollout, or one depends on another), that'd be helpful to understand. Otherwise, I'd suggest squashing them into one PR for a cleaner review and merge.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs Human Review

Possibly: The initialize handler (lines 263-278) silently swallows setReasoningEffort errors via try/catch, while handleSetEffort re-throws. This matches the existing convention for other optional init steps (MCP servers, subagents), but the inconsistency means an invalid effort at init time is silently ignored while the same value at runtime correctly throws. Consider surfacing the failure in the initialize response for consistency.

Possibly: The TypeScript SDK repeats the effort union type 'low' | 'medium' | 'high' | 'xhigh' | 'max' inline in 3+ locations (TransportOptions, QueryOptions, setEffort parameter) without a shared type alias. The Python SDK defined Effort: TypeAlias and reused it — consider doing the same for the TS SDK.

— qwen3.7-max via Qwen Code /review


try {
this.context.config.setReasoningEffort(
effort as 'low' | 'medium' | 'high' | 'xhigh' | 'max',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Missing server-side validation for effort value. handleSetEffort accepts any non-empty string and unsafely casts it to 'low' | 'medium' | 'high' | 'xhigh' | 'max' — but the as cast is erased at runtime. An arbitrary string like "banana" or "ultra" is passed directly to config.setReasoningEffort(), which stores it verbatim into the content generator config and eventually sends it to model API providers.

The existing normalizeReasoningEffort() utility in packages/core/src/core/reasoning-effort.ts handles aliases and rejects invalid values, but is never called here. The same unsafe cast also exists in the initialize handler at line 267.

Suggested change
effort as 'low' | 'medium' | 'high' | 'xhigh' | 'max',
const normalized = normalizeReasoningEffort(effort);
if (!normalized) {
throw new Error(
`Invalid effort: "${effort}". Expected one of: low, medium, high, xhigh, max.`,
);
}
this.context.config.setReasoningEffort(normalized);

— qwen3.7-max via Qwen Code /review

case 'interrupt':
case 'continue_last_turn':
case 'set_model':
case 'set_effort':

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Zero tests for the new set_effort functionality across all three packages:

  • CLI: No tests for handleSetEffort (success, invalid effort, aborted signal), the initialize-with-effort branch, or the case 'set_effort': routing in ControlDispatcher
  • TypeScript SDK: No test for Query.setEffort() — the existing setModel() test in Query.test.ts has full coverage but setEffort has none
  • Python SDK: No test for Query.set_effort(), no test for the new effort validation in validation.py, and the integration test fake CLI in conftest.py doesn't handle set_effort

Every new control-plane subtype should have at minimum: a routing test, a handler success/error test, and an SDK method test. The set_model tests provide a good template.

— qwen3.7-max via Qwen Code /review

throw new Error('Request aborted');
}

const effort = payload.effort;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The interactive /effort command routes input through normalizeReasoningEffort() which accepts aliases like "x-high", "maximum", "med", and is case-insensitive. The SDK path bypasses this entirely, doing a bare effort as ReasoningEffort cast.

Concrete downstream consequence: in clampReasoningEffort(), unrecognized strings have REASONING_EFFORT_RANKS[unknown] === undefined, so the rank comparison loop never matches and the function falls through to ranked[ranked.length - 1] — silently escalating to max (the most expensive tier). A user sending "x-high" through the SDK would get maximum cost without any feedback.

Suggested change
const effort = payload.effort;
const effort = normalizeReasoningEffort(payload.effort);
if (!effort) {
throw new Error(
`Invalid effort: ${JSON.stringify(payload.effort)}. Expected one of: low, medium, high, xhigh, max.`,
);
}

— qwen3.7-max via Qwen Code /review

}

const effort = payload.effort;
if (typeof effort !== 'string' || effort.trim() === '') {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] config.setReasoningEffort(undefined) is a documented operation that clears the effort override back to the model default. But this validation rejects empty/null, and neither SDK exposes a clear path. Once setEffort('high') is called, the user cannot revert to the model/provider default without creating a new session.

Consider accepting a sentinel value (e.g., "default") that maps to setReasoningEffort(undefined), or accept null/undefined as a valid effort in the protocol types.

— qwen3.7-max via Qwen Code /review

// Apply initial effort if provided
if (payload.effort) {
try {
this.context.config.setReasoningEffort(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] config.setReasoningEffort() early-returns without effect when reasoning === false (config.ts:3264: if (!cfg || cfg.reasoning === false) { return; }). But this code still logs "Initial effort set to: ..." and the initialize response includes can_set_effort: true in capabilities. The SDK consumer proceeds under the false assumption that their effort level is active.

Consider checking this.context.config.contentGeneratorConfig?.reasoning === false before calling and logging a warning, or having setReasoningEffort return a boolean indicating whether it was actually applied.

— qwen3.7-max via Qwen Code /review

await self._ensure_started()
await self._send_control_request("set_model", {"model": model})

async def set_effort(self, effort: str) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] validate_query_options() checks effort against _VALID_EFFORTS, but this runtime method accepts bare str with no validation. Users get a ValidationError for invalid effort in QueryOptions but can pass the same invalid value through set_effort() and only discover the problem when the server silently misconfigures (see clampReasoningEffort behavior with unknown strings).

Suggested change
async def set_effort(self, effort: str) -> None:
async def set_effort(self, effort: str) -> None:
if effort not in _VALID_EFFORTS:
raise ValidationError(
f"Invalid effort: {effort!r}. "
"Expected one of: low, medium, high, xhigh, max."
)
await self._ensure_started()
await self._send_control_request("set_effort", {"effort": effort})

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

💡 Suggestion: Consolidate SDK PRs

Hi @juhuan, thanks for the comprehensive SDK work! We noticed you have 15 open PRs that all modify the same core files (transport.py, types.py, queryOptionsSchema.ts, types.ts, ProcessTransport.ts, createQuery.ts) and were created on the same day.

The problem

  • Merge conflicts: Since all 15 PRs touch the same files, whichever merges first will cause conflicts in the remaining 14.
  • Review overhead: Reviewing 15 near-identical PRs separately is inefficient and risks fatigue.
  • CI cost: 15 separate CI runs for the same lint/typecheck passes.

Suggestion: regroup into 2 PRs

We recommend closing the current 15 PRs and reopening them as 2 consolidated PRs:

PR 1 — feat(sdk): expose transport and query options in both SDKs

Covers pure SDK-side option additions (~9 current PRs):

PR 2 — feat(sdk): add control request methods to both SDKs

Covers features that also involve CLI-side ControlDispatcher changes (~4 current PRs):

This keeps a reasonable separation of concerns while eliminating the merge-conflict chain and making review much more manageable.

/cc @juhuan

@wenshao

wenshao commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Closing in favor of consolidated PRs (see suggestion comment above). Please reopen as 2 grouped PRs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants