feat(stats): add /stats usage dashboard + Z AI provider auth - #75
Conversation
- Remove stray extra ']' in the /logout usage string (rendered a literal ']' in the Rich-formatted help output). - Apply ruff format to tests/auth/test_z_ai_auth.py (double-space before inline comments was failing the ruff format --check CI gate).
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds an interactive /stats TUI (session collection, pricing, insights), enriches StatusUpdate with optional per-step ChangesUsage Attribution & Analytics Dashboard
Managed LLM Provider Integrations
Sequence Diagram(s)sequenceDiagram
participant Shell as Shell/User
participant Login as login_z_ai_api_key
participant Discover as _discover_z_ai_models
participant Parser as _parse_discovered_models
participant Config as Config
participant Save as save_config
Shell->>Login: provide API key
Login->>Discover: GET /models (with API key)
Discover-->>Parser: JSON payload
Parser-->>Login: parsed ZaiModel list
Login->>Config: apply provider & models (apply_z_ai_models/_apply_z_ai_config)
Config->>Save: persist updated config
Login-->>Shell: yield OAuthEvent (success/error)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/pythinker_code/auth/__init__.py (1)
13-26:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winFix missing
MOONSHOT_PLATFORM_IDinpythinker_code.authexports (runtime import error)
src/pythinker_code/auth/moonshot.pyimportsMOONSHOT_PLATFORM_IDfrompythinker_code.auth, butsrc/pythinker_code/auth/__init__.pynever defines and doesn’t exportMOONSHOT_PLATFORM_ID, so importing the Moonshot provider can fail immediately.Proposed fix
OLLAMA_PLATFORM_ID = "ollama" +MOONSHOT_PLATFORM_ID = "moonshot" ZAI_PLATFORM_ID = "z-ai" __all__ = [ "ANTHROPIC_PLATFORM_ID", "DEEPSEEK_PLATFORM_ID", "LM_STUDIO_PLATFORM_ID", "MINIMAX_PLATFORM_ID", + "MOONSHOT_PLATFORM_ID", "OLLAMA_PLATFORM_ID", "OPENAI_API_PLATFORM_ID", "OPENAI_CHATGPT_PLATFORM_ID", "OPENCODE_GO_PLATFORM_ID",🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pythinker_code/auth/__init__.py` around lines 13 - 26, Define the missing MOONSHOT_PLATFORM_ID constant in the module and export it: add a line like MOONSHOT_PLATFORM_ID = "moonshot" near the other platform id constants and include the string "MOONSHOT_PLATFORM_ID" in the __all__ list so imports from pythinker_code.auth (used by moonshot.py) succeed at runtime.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@CHANGELOG.md`:
- Line 19: Add a new bullet in the Unreleased section of CHANGELOG.md mirroring
the Z AI provider auth line to document Moonshot provider auth; specifically
mention "Moonshot provider auth" and list login/logout via API key, model
discovery, OAuth selector wired into the TUI, and that it is hooked into
`refresh_managed_models` so release notes reflect the shipped integration.
In `@src/pythinker_code/auth/moonshot.py`:
- Around line 18-19: Replace the hard-coded managed provider and model strings
by using the shared helpers: stop using MOONSHOT_PROVIDER_KEY and
MOONSHOT_DEFAULT_MODEL_ALIAS inline and construct the provider key with
managed_provider_key("moonshot") (and validate/parse with
parse_managed_provider_key() where needed) and construct the default model alias
with managed_model_key("moonshot", "kimi-k2.6"); update any usage sites that
expect the raw strings to use these helpers so the code follows the managed
key/model convention.
In `@src/pythinker_code/auth/z_ai.py`:
- Around line 96-101: _parse_discovered_models currently returns an empty tuple
for malformed or empty discovery payloads which makes callers (like
refresh_z_ai_models) treat that as "zero models" and prune existing models;
change _parse_discovered_models to return None (Optional[tuple[ZaiModel, ...]])
when the payload is malformed or missing expected "data" rather than an empty
tuple, and update refresh_z_ai_models (and the other call site around the
similar logic at the other occurrence) to treat None as "no refresh / skip
update" while still treating an actual empty list (explicit discovery -> []) as
a valid zero-model result to be applied. Ensure references: function
_parse_discovered_models and caller refresh_z_ai_models (and the duplicate block
at the other occurrence) are updated accordingly.
- Around line 19-20: Replace hard-coded literals ZAI_PROVIDER_KEY and
ZAI_DEFAULT_MODEL_ALIAS with values constructed using the shared managed-key
helpers: call managed_provider_key("z-ai") to build the provider key and
managed_model_key("z-ai", "glm-5.1") (or the equivalent helper that accepts
platform_id and model_id) for the default model alias; update references to
ZAI_PROVIDER_KEY and ZAI_DEFAULT_MODEL_ALIAS accordingly and remove the
duplicated string literals so future changes to managed key format are
centralized in the helper functions (see managed_provider_key() and
managed_model_key()/parse_managed_provider_key()).
In `@src/pythinker_code/ui/shell/oauth.py`:
- Line 135: The PR only added OAuthProviderEntry(id="z-ai", ...) but omitted the
Moonshot provider wiring; add OAuthProviderEntry(id="moonshot", name="Moonshot",
auth_type="oauth") alongside the z-ai entry and ensure render_provider_selector
(or the providers list used to populate the TUI selector) includes this new
entry; then add a '/login moonshot' branch in the login handler (e.g.,
handle_login or start_auth_flow) that invokes the OAuth flow (call
start_oauth_flow or the existing oauth_login routine) and persist the returned
tokens, and add a '/logout moonshot' branch in the logout handler (e.g.,
handle_logout or revoke_auth) that clears/revokes Moonshot tokens (call
revoke_oauth_token or clear_provider_credentials); finally update any
provider-specific display/update functions (e.g., authenticate_provider or
provider_info_display) to handle id="moonshot" so the selector, login, and
logout flows all work for Moonshot.
In `@src/pythinker_code/ui/shell/stats_collector.py`:
- Around line 77-79: The aggregated tokens properties are omitting cached-read
tokens (input_cache_read) causing undercounting; update the tokens getter(s) to
include input_cache_read alongside input_other, output, and
input_cache_creation—specifically modify the tokens property and the analogous
aggregate property at the other occurrence (lines referenced) so they sum
input_cache_read as well; reference the tokens property and the
StepRecord.total_tokens / input_cache_read field to locate the exact places to
change.
- Around line 245-246: The except OSError: return silently swallows failures
when reading wire files (wire_path) — change the handler to capture the
exception (except OSError as e) and either log a warning/error including the
wire_path and exception (use the module/class logger used elsewhere, e.g.
self.logger or logger) or raise/surface a partial-load warning to the caller so
the session usage is not underreported; update the handler in the stats
collector function/method that reads wire_path (the block with "except OSError")
to include the wire_path and exception details in the log or propagate a
specific PartialLoadWarning/exception.
- Around line 227-230: The dedupe uses a weak hash (h) of only timestamp and
total so seen_hashes (shared in load_all_stats()) collapses distinct
StatusUpdate events; update the dedupe key to include a stable event identifier
when present (e.g., event_id or StatusUpdate.id/session_id) or, if not
available, include session_id plus the individual token fields (e.g.,
prompt_tokens, completion_tokens, total) and timestamp so that seen_hashes
distinguishes events across sessions and token splits (modify the h construction
and where seen_hashes is used in load_all_stats()/StatusUpdate processing).
In `@src/pythinker_code/ui/shell/stats.py`:
- Around line 325-329: The code is blocking the event loop by calling
load_all_stats() synchronously and also swallows errors; change it to run the
loader off the event loop with await asyncio.to_thread(load_all_stats) and
ensure the exception is logged (e.g., using the module logger) before calling
console.print to show the UI error; keep catching Exception to present the UI
message but log the full exception details (traceback) first so collector bugs
are recorded.
In `@tests/auth/test_z_ai_auth.py`:
- Around line 178-182: The test is mocking the private helper
_discover_z_ai_models directly; instead stub the HTTP/session boundary used by
login_z_ai_api_key so internal refactors of z_ai.py won't break. Replace
monkeypatch.setattr("pythinker_code.auth.z_ai._discover_z_ai_models", ...) with
a monkeypatch that makes the aiohttp ClientSession request used by
login_z_ai_api_key raise aiohttp.ClientConnectionError (e.g., monkeypatch the
ClientSession._request or the specific session.post/get method the code uses) so
the observable behavior (login_z_ai_api_key raising/logging on connection error)
is preserved without touching the private helper.
In `@tests/ui_and_conv/test_stats_collector.py`:
- Around line 114-116: Update the test to assert observable contracts of
get_sessions_root() rather than a trivially-true non-None check: set a known
PYTHINKER_DIR (use pytest monkeypatch or temporarily set os.environ), call
get_sessions_root(), and assert the returned Path is rooted in that directory
(starts with or has the env value as a parent) and ends with the "sessions"
component (e.g., path.name == "sessions" or path.parts[-1] == "sessions"); keep
the existence check optional since the directory may not be created.
In `@tests/ui_and_conv/test_stats_pricing.py`:
- Around line 42-46: The test test_prefix_match_fallback is only hitting the
exact-match path because "claude-sonnet-4-5-20250929" exists in _PRICE_TABLE;
change the model id passed to get_cost_usd to a non-existent suffixed alias
(e.g. "claude-sonnet-4-5-20250929-extra") so the lookup must fall back to the
"claude-sonnet-4-5" prefix, keep using _usage to build usage and assert cost >
0.0 to validate the prefix-fallback behavior.
---
Outside diff comments:
In `@src/pythinker_code/auth/__init__.py`:
- Around line 13-26: Define the missing MOONSHOT_PLATFORM_ID constant in the
module and export it: add a line like MOONSHOT_PLATFORM_ID = "moonshot" near the
other platform id constants and include the string "MOONSHOT_PLATFORM_ID" in the
__all__ list so imports from pythinker_code.auth (used by moonshot.py) succeed
at runtime.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 08a98a40-a102-401a-9ff5-e073ab5c7427
📒 Files selected for processing (16)
CHANGELOG.mdsrc/pythinker_code/auth/__init__.pysrc/pythinker_code/auth/moonshot.pysrc/pythinker_code/auth/platforms.pysrc/pythinker_code/auth/z_ai.pysrc/pythinker_code/soul/pythinkersoul.pysrc/pythinker_code/ui/shell/oauth.pysrc/pythinker_code/ui/shell/slash.pysrc/pythinker_code/ui/shell/stats.pysrc/pythinker_code/ui/shell/stats_collector.pysrc/pythinker_code/ui/shell/stats_pricing.pysrc/pythinker_code/wire/types.pytests/auth/test_z_ai_auth.pytests/core/test_wire_message.pytests/ui_and_conv/test_stats_collector.pytests/ui_and_conv/test_stats_pricing.py
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/pythinker_code/auth/z_ai.py`:
- Around line 168-170: The code currently forces config.default_thinking = False
whenever config.default_thinking_effort is None; instead treat None as
“unconfigured” and leave the legacy boolean alone. Change the logic around
config.default_thinking_effort so that you only override config.default_thinking
when effort is explicitly set (e.g., "off" -> set False, "on"/explicit values ->
set True), and if config.default_thinking_effort is None do not mutate
config.default_thinking or coerce the effort to "off". Update the branch
referencing config.default_thinking_effort and config.default_thinking
accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 70ed43e2-c87b-4a94-bded-a3837005175f
📒 Files selected for processing (5)
src/pythinker_code/auth/__init__.pysrc/pythinker_code/auth/z_ai.pysrc/pythinker_code/llm.pytests/auth/test_z_ai_auth.pytests/core/test_create_llm.py
- stats_collector: fix tokens undercounting (add input_cache_read), include session_id in dedup hash, log OSError instead of silently swallowing - stats: run load_all_stats via asyncio.to_thread to avoid blocking event loop - z_ai/moonshot: use managed_provider_key/managed_model_key helpers instead of hard-coded strings; _parse_discovered_models returns None for structurally invalid payloads (prevents spurious model prune on malformed API response) - oauth: wire Moonshot into /login and /logout selector, dispatch, and help text - tests: fix test_prefix_match_fallback to exercise actual prefix fallback; fix test_get_sessions_root_exists to assert path structure; update test_parse_discovered_z_ai_models_handles_payloads for None semantics
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/pythinker_code/auth/moonshot.py (1)
127-139:⚠️ Potential issue | 🟠 Major | ⚡ Quick winMoonshot not wired into
refresh_managed_models(and generic refresh can’t reach it)CHANGELOG.md says Moonshot is “wired into…
refresh_managed_models”, butsrc/pythinker_code/auth/platforms.pyrefresh logic only has explicit refresh/apply for OpenCode Go, MiniMax, and Z AI—Moonshot (managed:moonshot) isn’t handled. Additionally,get_platform_by_id("moonshot")will returnNonebecausePLATFORMShas nomoonshotentry, so the generic managed-provider refresh loop skips Moonshot entirely.Add Moonshot into the managed refresh path (via a Moonshot-specific
refresh_*/apply_*or by registeringmoonshotinPLATFORMSso generic discovery runs).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pythinker_code/auth/moonshot.py` around lines 127 - 139, The refresh path currently skips Moonshot because PLATFORMS has no "moonshot" entry and refresh_managed_models (and get_platform_by_id) only knows about the existing OpenCode, MiniMax, and Z AI handlers; add Moonshot to the managed refresh path by either (A) registering a platform entry for "moonshot" in PLATFORMS so get_platform_by_id("moonshot") returns a platform that uses the generic discovery/apply flow, or (B) implement Moonshot-specific handlers analogous to the existing refresh_* / apply_* functions used for OpenCode Go/MiniMax/ZAI and call them from refresh_managed_models; update refresh_managed_models, get_platform_by_id, and PLATFORMS accordingly so managed:moonshot is discovered and applied.
♻️ Duplicate comments (2)
src/pythinker_code/ui/shell/stats_collector.py (2)
371-373:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPreserve the parent session in subagent IDs.
For subagent wires this produces
subagents/<agent_id>, which collides across different parent sessions and skews session counts and concentration insights.Proposed fix
- session_id = f"{wire_path.parent.parent.name}/{wire_path.parent.name}" + session_id = str(wire_path.relative_to(root).parent)Based on learnings, "Subagent instances are persisted separately under
session/subagents/<agent_id>/; parent sessions should ingest summarized evidence rather than full noisy logs".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pythinker_code/ui/shell/stats_collector.py` around lines 371 - 373, Currently session_id is built as f"{wire_path.parent.parent.name}/{wire_path.parent.name}" which collapses subagent wires across different parent sessions; update the session_id construction in the loop over wire_files so that subagent wires include the parent session component (e.g. include the higher-level parent folder name or session identifier) before calling parse_wire_file, ensuring uniqueness per parent session. Locate the loop and the session_id variable and adjust the logic that detects subagent paths (used by parse_wire_file) so subagent IDs become session/subagents/<parent_session>/<agent_id> and ensure parent sessions instead ingest summarized evidence rather than full subagent logs when generating session-level metrics.
228-231:⚠️ Potential issue | 🟠 Major | ⚡ Quick winStrengthen the dedupe key.
Line 228 still dedupes on
session_id + timestamp + total, so two distinctStatusUpdateevents in the same session with the same timestamp and total tokens will be dropped.Proposed fix
- total = input_other + output + cache_read + cache_write - ts = float(obj.get("timestamp", 0)) - - h = f"{session_id}:{ts}:{total}" - if h in seen_hashes: - continue - seen_hashes.add(h) - model_name: str = str(payload.get("model_name") or "unknown") provider_key: str = str(payload.get("provider_key") or "unknown") + message_id = str(payload.get("message_id") or "") + ts = float(obj.get("timestamp", 0)) + h = ( + f"{session_id}:{ts}:{message_id}:{input_other}:{output}:" + f"{cache_read}:{cache_write}:{model_name}:{provider_key}" + ) + if h in seen_hashes: + continue + seen_hashes.add(h)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pythinker_code/ui/shell/stats_collector.py` around lines 228 - 231, The current dedupe key h = f"{session_id}:{ts}:{total}" is too weak — update events with identical session_id, timestamp and total can collide; modify the dedupe key in the stats collection logic to include a unique identifier for the StatusUpdate (e.g., update.id, update.uuid, or a deterministic hash/serialized form of the StatusUpdate payload) so it becomes something like f"{session_id}:{ts}:{total}:{unique_update_id}", and use that when checking/adding to seen_hashes (referencing variables session_id, ts, total, seen_hashes and the StatusUpdate instance).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/pythinker_code/auth/moonshot.py`:
- Around line 127-139: The refresh path currently skips Moonshot because
PLATFORMS has no "moonshot" entry and refresh_managed_models (and
get_platform_by_id) only knows about the existing OpenCode, MiniMax, and Z AI
handlers; add Moonshot to the managed refresh path by either (A) registering a
platform entry for "moonshot" in PLATFORMS so get_platform_by_id("moonshot")
returns a platform that uses the generic discovery/apply flow, or (B) implement
Moonshot-specific handlers analogous to the existing refresh_* / apply_*
functions used for OpenCode Go/MiniMax/ZAI and call them from
refresh_managed_models; update refresh_managed_models, get_platform_by_id, and
PLATFORMS accordingly so managed:moonshot is discovered and applied.
---
Duplicate comments:
In `@src/pythinker_code/ui/shell/stats_collector.py`:
- Around line 371-373: Currently session_id is built as
f"{wire_path.parent.parent.name}/{wire_path.parent.name}" which collapses
subagent wires across different parent sessions; update the session_id
construction in the loop over wire_files so that subagent wires include the
parent session component (e.g. include the higher-level parent folder name or
session identifier) before calling parse_wire_file, ensuring uniqueness per
parent session. Locate the loop and the session_id variable and adjust the logic
that detects subagent paths (used by parse_wire_file) so subagent IDs become
session/subagents/<parent_session>/<agent_id> and ensure parent sessions instead
ingest summarized evidence rather than full subagent logs when generating
session-level metrics.
- Around line 228-231: The current dedupe key h = f"{session_id}:{ts}:{total}"
is too weak — update events with identical session_id, timestamp and total can
collide; modify the dedupe key in the stats collection logic to include a unique
identifier for the StatusUpdate (e.g., update.id, update.uuid, or a
deterministic hash/serialized form of the StatusUpdate payload) so it becomes
something like f"{session_id}:{ts}:{total}:{unique_update_id}", and use that
when checking/adding to seen_hashes (referencing variables session_id, ts,
total, seen_hashes and the StatusUpdate instance).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 7ddcbfd4-e46e-4e86-836a-9c87bcd7cc04
📒 Files selected for processing (9)
CHANGELOG.mdsrc/pythinker_code/auth/moonshot.pysrc/pythinker_code/auth/z_ai.pysrc/pythinker_code/ui/shell/oauth.pysrc/pythinker_code/ui/shell/stats.pysrc/pythinker_code/ui/shell/stats_collector.pytests/auth/test_z_ai_auth.pytests/ui_and_conv/test_stats_collector.pytests/ui_and_conv/test_stats_pricing.py
…agent session ID - Add model_name/provider_key to all e2e StatusUpdate payload snapshots (37 occurrences across 6 test files); plan_mode=None events get None/None, step-completion events get scripted_echo/scripted_provider (or provider-b for test_model_override with --model model-b) - Fix subagent session ID derivation in load_all_stats: subagent wire paths (sessions/wdhash/sessid/subagents/agentid/wire.jsonl) were producing session_id='subagents/agentid' instead of 'wdhash/sessid', collapsing distinct parent sessions with same subagent ID
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/pythinker_code/ui/shell/stats_collector.py`:
- Around line 375-378: The deduplication key currently collapses parent and
subagent StatusUpdate records because session_id and token_usage.total are too
coarsely combined; update the dedup key calculation in
stats_collector.parse_wire_file() / load_all_stats() (where seen_hashes is
populated and session_id is constructed) to include more discriminating fields
such as message_id and individual token_usage components (input_other, output,
input_cache_read, input_cache_creation) instead of only token_usage.total, so
parent and subagent StatusUpdate entries cannot collide even if timestamp and
total match; ensure the new key still includes session_id (using the existing
wire_path parents logic) and use that augmented key when adding/checking
seen_hashes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 51a162e5-44ff-4fb5-bbd0-6f25bd8b2c75
📒 Files selected for processing (7)
src/pythinker_code/ui/shell/stats_collector.pytests_e2e/test_wire_approvals_tools.pytests_e2e/test_wire_config.pytests_e2e/test_wire_prompt.pytests_e2e/test_wire_protocol.pytests_e2e/test_wire_sessions.pytests_e2e/test_wire_skills_mcp.py
- thinking: apply_login_thinking_defaults now preserves legacy default_thinking=True when provider default is False; fixes silent downgrade for users on the legacy boolean path across all providers (z_ai, moonshot, deepseek, opencode_go, minimax, openrouter) - z_ai: remove redundant manual guard (apply_login_thinking_defaults now handles this correctly at the shared layer) - stats_collector: strengthen dedup key from session_id:ts:total to session_id:ts:input_other:output:cache_read:cache_write so distinct steps with equal totals cannot collide even across parent/subagent - tests: add test_apply_z_ai_config_preserves_legacy_thinking_true to cover the preserved-thinking case
Summary
refresh_managed_models/statsusage dashboard: interactive prompt_toolkit TUI showing token + cost breakdown by provider/model across Today / This Week / Last Week / All Timestats_pricing.py):get_cost_usd(model, usage)with prefix-match fallback for versioned aliases, covering Anthropic, OpenAI, DeepSeek, GLM, Kimi, MiniMax, Geministats_collector.py): walks~/.pythinker/sessions/(including subagent wires), parsesStatusUpdateevents, deduplicates, bins into time periods, computes cost-weighted insightsStatusUpdatewire extension: adds optionalmodel_name/provider_keyfields for per-step attribution; existing sessions fall back to"unknown"StatusUpdateTest Plan
make check-pythinker-codepasses (ruff + pyright strict + ty)pytest tests/ui_and_conv/test_stats_pricing.py tests/ui_and_conv/test_stats_collector.py— 14 passedpytest tests/core/test_wire_message.py— serde snapshot updated, 10 passedtest_plan_mode_injection_providerunrelated to this branch)/statsopens dashboard, tabs switch with Tab/←→,vtoggles insights,qclosesSummary by CodeRabbit
New Features
/stats(alias/history) TUI showing usage, token and cost breakdowns by provider/model across multiple time ranges, built from local session data.New Providers
Enhancements
Tests