Skip to content

feat(sdk): add get_usage_info() control request to CLI and both SDKs - #6473

Closed
juhuan wants to merge 1 commit into
QwenLM:mainfrom
juhuan:feat/sdk-get-usage-info
Closed

feat(sdk): add get_usage_info() control request to CLI and both SDKs#6473
juhuan wants to merge 1 commit into
QwenLM:mainfrom
juhuan:feat/sdk-get-usage-info

Conversation

@juhuan

@juhuan juhuan commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Add a new get_usage_info control request that returns local usage statistics from the CLI's usage history service. Data includes token totals, per-model breakdown, daily trends, and heatmap data — the same data the /stats interactive command shows.

CLI:

  • Route get_usage_info to SystemController in ControlDispatcher
  • handleGetUsageInfo in SystemController calls loadUsageDashboard() from core
  • CLIControlGetUsageInfoRequest type with optional range field

Python SDK: get_usage_info(range="today") method on Query + CLIControlGetUsageInfoRequest protocol type

TypeScript SDK: getUsageInfo(range) method on Query + GET_USAGE_INFO in ControlRequestType enum + CLIControlGetUsageInfoRequest interface

Test plan

  • Python SDK tests pass (pytest — 58 passed)
  • TypeScript SDK typecheck passes (tsc --noEmit)
  • TypeScript SDK tests pass (vitest run — 1163 passed)

Add a new `get_usage_info` control request that returns local usage
statistics from the CLI's usage history service. Data includes token
totals, per-model breakdown, daily trends, and heatmap data.

CLI:
- Add `get_usage_info` routing in ControlDispatcher
- Add `handleGetUsageInfo` in SystemController (calls loadUsageDashboard)
- Add `CLIControlGetUsageInfoRequest` type

Python SDK:
- Add `get_usage_info(range="today")` method to Query
- Add `CLIControlGetUsageInfoRequest` protocol type

TypeScript SDK:
- Add `GET_USAGE_INFO` to ControlRequestType enum
- Add `getUsageInfo(range)` method to Query
- Add `CLIControlGetUsageInfoRequest` interface

@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.

Thanks for the PR, @juhuan!

Unfortunately the PR body doesn't follow the required template. Several required sections are missing:

  • Why it's needed — what's the motivation or user-facing benefit?
  • Reviewer Test PlanHow to verify, Evidence (Before & After), Tested on, Environment
  • Risk & Scope — tradeoffs, out-of-scope items, breaking changes
  • Linked Issues — any related issue this addresses

The "Summary" and "Test plan" headings don't match the template structure. Could you fill in the template sections so reviewers have enough context to evaluate the change?

中文说明

感谢贡献 @juhuan

PR 描述缺少模板中的必填部分:

  • Why it's needed — 动机或用户价值是什么?
  • Reviewer Test Plan — 包括 How to verifyEvidence (Before & After)Tested onEnvironment
  • Risk & Scope — 取舍、不在范围内的内容、破坏性变更
  • Linked Issues — 是否有关联的 issue?

"Summary" 和 "Test plan" 标题与模板结构不匹配。请补充模板中的各个部分,方便 reviewer 评估。

Qwen Code · qwen3.7-max

@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.

Also noting:

  • No tests were added for get_usage_info in any package (CLI controller, TS SDK, or Python SDK). Consider adding at minimum a unit test for SystemController.handleGetUsageInfo and SDK wire-format tests for both getUsageInfo() / get_usage_info().
  • Missing capability advertisement: buildControlCapabilities() has can_get_context_usage: true but no corresponding can_get_usage_info entry. SDK clients using capability discovery won't know this feature exists.
  • Missing Python sync wrapper: SyncQuery in sync_query.py wraps every other control request method but not get_usage_info().


class CLIControlGetUsageInfoRequest(TypedDict):
subtype: Literal["get_usage_info"]
range: NotRequired[str]

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] range is typed as NotRequired[str], accepting any string. The TypeScript SDK constrains it to 'today' | 'week' | 'month' | 'all', and Literal is already imported in this file. An invalid value (e.g., "daily") silently falls through to all-time data in getTimeRangeBounds's default arm.

Suggested change
range: NotRequired[str]
range: NotRequired[Literal["today", "week", "month", "all"]]

— qwen3.7-max via Qwen Code /review

return await self._send_control_request("mcp_server_status")

async def get_usage_info(
self, range: str = "today"

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] Same issue as protocol.pyrange should use Literal to match the TypeScript SDK's union type. Add Literal to the typing imports and constrain this parameter:

Suggested change
self, range: str = "today"
async def get_usage_info(
self, range: Literal["today", "week", "month", "all"] = "today"
) -> dict[str, Any] | None:

— qwen3.7-max via Qwen Code /review

throw new Error('Request aborted');
}

const range = payload.range ?? 'today';

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] No runtime validation of range before passing to loadUsageDashboard. The as CLIControlGetUsageInfoRequest cast is compile-time only — the payload arrives as JSON over the wire. getTimeRangeBounds has a default switch arm that returns new Date(0), so an invalid range string silently returns all historical data. Add a guard:

Suggested change
const range = payload.range ?? 'today';
const range = payload.range ?? 'today';
const validRanges = ['today', 'week', 'month', 'all'] as const;
if (!(validRanges as readonly string[]).includes(range)) {
throw new Error(`Invalid range: ${range}. Must be one of: ${validRanges.join(', ')}`);
}

— qwen3.7-max via Qwen Code /review

}

const range = payload.range ?? 'today';
const dashboard = await loadUsageDashboard({ range });

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] No caching. loadUsageDashboard reads and parses the entire usage_record.jsonl from disk on every call. The JSDoc on loadUsageDashboard explicitly recommends caching: "The load can be I/O heavy on large histories, so callers should cache". The daemon route in routes/usage-stats.ts implements a 60s TTL cache — this control-request path should do the same.

— qwen3.7-max via Qwen Code /review

}

return {
subtype: 'get_usage_info',

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] ...dashboard spreads the entire UsageDashboard interface (which includes a 183-day heatmap, all models, all skills) into the wire response. Any future field added to UsageDashboard (e.g., per-project data, raw records) auto-leaks into the SDK response with no compile-time warning. Consider explicitly picking fields or defining a response DTO to create a deliberate boundary.

— 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.

3 participants