Skip to content

feat(stats): add /stats usage dashboard + Z AI provider auth - #75

Merged
elkaix merged 22 commits into
mainfrom
feat/z-ai-provider
Jun 4, 2026
Merged

feat(stats): add /stats usage dashboard + Z AI provider auth#75
elkaix merged 22 commits into
mainfrom
feat/z-ai-provider

Conversation

@elkaix

@elkaix elkaix commented Jun 4, 2026

Copy link
Copy Markdown
Member

Summary

  • Z AI provider auth: login/logout via API key, model discovery, OAuth selector in TUI, wired into refresh_managed_models
  • /stats usage dashboard: interactive prompt_toolkit TUI showing token + cost breakdown by provider/model across Today / This Week / Last Week / All Time
  • Pricing table (stats_pricing.py): get_cost_usd(model, usage) with prefix-match fallback for versioned aliases, covering Anthropic, OpenAI, DeepSeek, GLM, Kimi, MiniMax, Gemini
  • Session collector (stats_collector.py): walks ~/.pythinker/sessions/ (including subagent wires), parses StatusUpdate events, deduplicates, bins into time periods, computes cost-weighted insights
  • StatusUpdate wire extension: adds optional model_name / provider_key fields for per-step attribution; existing sessions fall back to "unknown"
  • 14 new tests across pricing and collector; updated serde snapshot for StatusUpdate

Test Plan

  • make check-pythinker-code passes (ruff + pyright strict + ty)
  • pytest tests/ui_and_conv/test_stats_pricing.py tests/ui_and_conv/test_stats_collector.py — 14 passed
  • pytest tests/core/test_wire_message.py — serde snapshot updated, 10 passed
  • Full suite: 4377 passed (1 pre-existing failure in test_plan_mode_injection_provider unrelated to this branch)
  • /stats opens dashboard, tabs switch with Tab/←→, v toggles insights, q closes

Summary by CodeRabbit

  • New Features

    • Interactive /stats (alias /history) TUI showing usage, token and cost breakdowns by provider/model across multiple time ranges, built from local session data.
  • New Providers

    • Z AI and Moonshot support with API-key login/logout, model discovery, and selectable provider integration in the UI.
  • Enhancements

    • Per-step attribution: status updates may include model and provider identifiers.
    • Pricing & cost estimation for token usage.
    • Kimi “thinking” models treated as always-on where applicable.
  • Tests

    • Expanded coverage for auth, model discovery, stats collection, and pricing.

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an interactive /stats TUI (session collection, pricing, insights), enriches StatusUpdate with optional per-step model_name and provider_key, and introduces Z AI and Moonshot managed providers with API-key login/logout, model discovery, config apply/prune, shell UI integration, and tests.

Changes

Usage Attribution & Analytics Dashboard

Layer / File(s) Summary
Wire schema & data enrichment
src/pythinker_code/wire/types.py, src/pythinker_code/soul/pythinkersoul.py, tests/core/test_wire_message.py
StatusUpdate gains optional model_name and provider_key; PythinkerSoul._step populates them; serialization tests updated.
Pricing table & cost helper
src/pythinker_code/ui/shell/stats_pricing.py
Adds _PRICE_TABLE and get_cost_usd computing USD-per-step with exact and longest-prefix model lookup.
Session collection & aggregation
src/pythinker_code/ui/shell/stats_collector.py
Discovers wire.jsonl under sessions dir, parses StatusUpdate steps into StepRecord, deduplicates, aggregates per-model/provider totals, bins into periods, and computes cost-weighted insights.
Interactive dashboard & stats command
src/pythinker_code/ui/shell/stats.py, src/pythinker_code/ui/shell/slash.py
Implements StatsApp prompt-toolkit TUI (table and insights views), keybindings, rendering, and /stats (alias /history) slash command.
Session collection & pricing tests
tests/ui_and_conv/test_stats_collector.py, tests/ui_and_conv/test_stats_pricing.py
Tests for wire parsing/extraction, deduplication, defaulting missing fields, period binning, session discovery including nested subagents, load_all_stats, and pricing math.

Managed LLM Provider Integrations

Layer / File(s) Summary
Z AI provider core & discovery
src/pythinker_code/auth/__init__.py, src/pythinker_code/auth/z_ai.py
Adds ZAI_PLATFORM_ID, ZaiModel, built-in GLM catalog, env API-key reader, discovery parsing, apply/prune/refresh helpers, and login/logout async generators.
Moonshot provider core & discovery
src/pythinker_code/auth/moonshot.py
Adds MoonshotModel, env-key reader, discovery parsing, config application, and login/logout async iterators.
OAuth shell UI provider selection
src/pythinker_code/ui/shell/oauth.py
Extends /login and /logout to accept z-ai and Moonshot: adds provider entries, maps provider keys, prompts for API keys, and invokes respective handlers.
Managed provider refresh & persistence
src/pythinker_code/auth/platforms.py
Integrates Z AI into refresh_managed_models (skip generic discovery for Z AI, call provider-specific refresh/apply, and persist config when changed).
Z AI authentication & config tests
tests/auth/test_z_ai_auth.py
Comprehensive tests for Z AI model catalog, environment key parsing, discovery parsing and fallbacks, config application, login/logout behavior (including 401 handling), and model reconciliation/pruning.
Release notes
CHANGELOG.md
Unreleased section documents /stats dashboard, StatusUpdate attribution fields, and Z AI/Moonshot provider auth/discovery.
Kimi 'always-thinking' capability & tests
src/pythinker_code/llm.py, tests/core/test_create_llm.py
derive_model_capabilities marks kimi-k2-thinking as always_thinking; tests verify create_llm enforces thinking and payload flags.

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)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

enhancement

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.81% which is insufficient. The required threshold is 70.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title follows conventional commits format with feat type, stats scope, and clearly describes the two main features: /stats usage dashboard and Z AI provider auth.
Description check ✅ Passed Description provides comprehensive summary of all changes, detailed test results, and addresses the provided template sections including linked issue reference and checklist completion.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/z-ai-provider

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 win

Fix missing MOONSHOT_PLATFORM_ID in pythinker_code.auth exports (runtime import error)

src/pythinker_code/auth/moonshot.py imports MOONSHOT_PLATFORM_ID from pythinker_code.auth, but src/pythinker_code/auth/__init__.py never defines and doesn’t export MOONSHOT_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

📥 Commits

Reviewing files that changed from the base of the PR and between 5066a83 and b400a98.

📒 Files selected for processing (16)
  • CHANGELOG.md
  • src/pythinker_code/auth/__init__.py
  • src/pythinker_code/auth/moonshot.py
  • src/pythinker_code/auth/platforms.py
  • src/pythinker_code/auth/z_ai.py
  • src/pythinker_code/soul/pythinkersoul.py
  • src/pythinker_code/ui/shell/oauth.py
  • src/pythinker_code/ui/shell/slash.py
  • src/pythinker_code/ui/shell/stats.py
  • src/pythinker_code/ui/shell/stats_collector.py
  • src/pythinker_code/ui/shell/stats_pricing.py
  • src/pythinker_code/wire/types.py
  • tests/auth/test_z_ai_auth.py
  • tests/core/test_wire_message.py
  • tests/ui_and_conv/test_stats_collector.py
  • tests/ui_and_conv/test_stats_pricing.py

Comment thread CHANGELOG.md
Comment thread src/pythinker_code/auth/moonshot.py Outdated
Comment thread src/pythinker_code/auth/z_ai.py Outdated
Comment thread src/pythinker_code/auth/z_ai.py Outdated
Comment thread src/pythinker_code/ui/shell/oauth.py
Comment thread src/pythinker_code/ui/shell/stats_collector.py
Comment thread src/pythinker_code/ui/shell/stats.py
Comment thread tests/auth/test_z_ai_auth.py
Comment thread tests/ui_and_conv/test_stats_collector.py Outdated
Comment thread tests/ui_and_conv/test_stats_pricing.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 4f344bb and 8ea8b52.

📒 Files selected for processing (5)
  • src/pythinker_code/auth/__init__.py
  • src/pythinker_code/auth/z_ai.py
  • src/pythinker_code/llm.py
  • tests/auth/test_z_ai_auth.py
  • tests/core/test_create_llm.py

Comment thread src/pythinker_code/auth/z_ai.py Outdated
- 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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 win

Moonshot not wired into refresh_managed_models (and generic refresh can’t reach it)

CHANGELOG.md says Moonshot is “wired into…refresh_managed_models”, but src/pythinker_code/auth/platforms.py refresh 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 return None because PLATFORMS has no moonshot entry, 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 registering moonshot in PLATFORMS so 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 win

Preserve 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 win

Strengthen the dedupe key.

Line 228 still dedupes on session_id + timestamp + total, so two distinct StatusUpdate events 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8ea8b52 and cb52f4c.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • src/pythinker_code/auth/moonshot.py
  • src/pythinker_code/auth/z_ai.py
  • src/pythinker_code/ui/shell/oauth.py
  • src/pythinker_code/ui/shell/stats.py
  • src/pythinker_code/ui/shell/stats_collector.py
  • tests/auth/test_z_ai_auth.py
  • tests/ui_and_conv/test_stats_collector.py
  • tests/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

codecov Bot commented Jun 4, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between cb52f4c and 66d9410.

📒 Files selected for processing (7)
  • src/pythinker_code/ui/shell/stats_collector.py
  • tests_e2e/test_wire_approvals_tools.py
  • tests_e2e/test_wire_config.py
  • tests_e2e/test_wire_prompt.py
  • tests_e2e/test_wire_protocol.py
  • tests_e2e/test_wire_sessions.py
  • tests_e2e/test_wire_skills_mcp.py

Comment thread src/pythinker_code/ui/shell/stats_collector.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
@elkaix
elkaix merged commit c6f9120 into main Jun 4, 2026
26 checks passed
@elkaix
elkaix deleted the feat/z-ai-provider branch June 4, 2026 02:27
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.

1 participant