feat: in-session model switching and cost awareness (#32)#35
Conversation
- Add per-session model override commands (set/get/clear) in preferences.rs - Add per-session token tracking (record_session_usage, get_session_usage) in process.rs - Add cost estimation module (cost.rs) with hardcoded pricing for common models - Add TokenBadge component showing session token usage and estimated cost - Add ModelSwitcher component for switching models per session - Wire TokenBadge and ModelSwitcher into Doctor page zeroclaw view - Register all new Tauri commands in lib.rs Closes #32
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 75675a040f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| STORE.get_or_init(|| Mutex::new(std::collections::HashMap::new())) | ||
| } | ||
|
|
||
| pub fn record_session_usage(session_id: &str, prompt_tokens: u64, completion_tokens: u64) { |
There was a problem hiding this comment.
Wire per-session usage recording into runtime calls
This new record_session_usage path is never invoked by the zeroclaw execution flow, which still updates only the global store via record_zeroclaw_usage in run_zeroclaw_message (src-tauri/src/runtime/zeroclaw/process.rs). As a result, get_session_usage_stats reads default-zero values for every session, so the per-session token/cost feature introduced in this change does not produce real data.
Useful? React with 👍 / 👎.
| pub fn set_session_model_override(session_id: String, model: String) -> Result<(), String> { | ||
| let trimmed = model.trim().to_string(); | ||
| if trimmed.is_empty() { | ||
| return Err("model must not be empty".into()); | ||
| } | ||
| if let Ok(mut map) = session_model_overrides().lock() { | ||
| map.insert(session_id, trimmed); |
There was a problem hiding this comment.
Apply session model override during zeroclaw execution
set_session_model_override stores overrides in-memory, but the runtime path does not read this map when selecting model/provider (it still uses global preference resolution in run_zeroclaw_message). That means the new model switch API updates state that never affects actual requests, so in-session model switching is functionally broken.
Useful? React with 👍 / 👎.
src/pages/Doctor.tsx
Outdated
| <TokenBadge sessionId="zeroclaw-doctor" model="gpt-4o" /> | ||
| <ModelSwitcher sessionId="zeroclaw-doctor" defaultModel="gpt-4o" /> |
There was a problem hiding this comment.
Use active doctor session key for usage/override widgets
Both widgets are bound to the fixed ID zeroclaw-doctor, but doctor sessions are created with a per-diagnosis UUID key in use-doctor-agent (sessionKeyRef.current = \agent:...:${crypto.randomUUID()}``). Because these IDs do not match the runtime session key, the badge/override calls target a different session namespace than live conversations, so usage remains empty and overrides cannot apply to the active diagnosis.
Useful? React with 👍 / 👎.
Keith-CY
left a comment
There was a problem hiding this comment.
Blocking findings
- defines per-session model overrides, but no model execution path uses them. The PR adds in (around lines 130-155) and /, but model selection calls are still based on global model preference and never consult per-session override. Result: changes UI state only and does not affect backend runtime model choice.
Impact: session-level model switcher is effectively non-functional.
- exposes , and adds /, but no execution path in runtime writes to per-session usage.
(around lines 280+) is never called from /streaming execution paths. As added, session usage stays zero and remains empty for real traffic.
Impact: Token/cost badge is misleading and non-functional in practice.
- hardcodes and when rendering / (around lines 1040-1042).
This is not guaranteed to match the actual per-session runtime ID or active model used for Doctor messages.
Impact: displayed usage/cost can be incorrect even if backend tracking were wired.
Please rework model override application + per-session usage recording before merge.
Keith-CY
left a comment
There was a problem hiding this comment.
Blocking findings
src-tauri/src/runtime/zeroclaw/process.rsdefines per-session model overrides, but no model execution path uses them. The PR addsset/get/clear_session_model_overrideinsrc-tauri/src/commands/preferences.rs, andset_session_model_override/get_session_model_overrideare registered insrc-tauri/src/lib.rs, but model selection remains global-only and never consults a session override. As a result, session model switching in UI does not affect actual runtime model selection.
Impact: session-level model switching is non-functional.
src-tauri/src/commands/preferences.rsexposesget_session_usage_stats, andsrc-tauri/src/runtime/zeroclaw/process.rsaddsrecord_session_usage/get_session_usage, but the runtime execution path never callsrecord_session_usagefor actual requests.
record_session_usage is added in src-tauri/src/runtime/zeroclaw/process.rs, yet no usage stats updates are wired into run_zeroclaw_once/streaming invocation paths. Session stats will stay zero for real sessions, so TokenBadge cannot show real usage/cost.
Impact: token and cost UI is misleading/non-functional.
src/pages/Doctor.tsxhardcodessessionId="zeroclaw-doctor"andmodel="gpt-4o"forTokenBadge/ModelSwitcher.
This ID/model may not map to the actual runtime session key or selected model, so usage/cost display can report the wrong session and wrong model.
Impact: displayed metrics can be incorrect even after the runtime wiring is fixed.
Please address these before merging.
e13a309 to
af6988c
Compare
|
Addressed all 3 blocking review findings: 1. Session model override now wired into runtime
2. Per-session usage recording now functional
3. Doctor.tsx uses dynamic session ID and model
|
The frontend passes instanceId as the session key for model override and usage tracking. The backend was using session_scope (which is the full storage_key like 'zeroclaw:doctor:local:...'), causing a key mismatch. Switch to instance_id which matches what the frontend sends. Addresses review feedback from Keith-CY on blocking findings 1 & 2.
|
Hi @Keith-CY, thanks for the thorough review — all three blocking findings were legit. Here's what I've fixed across the last two commits: Finding 1 (model override not wired into runtime):
Finding 2 (usage tracking not wired):
Finding 3 (hardcoded session/model in Doctor.tsx):
CI is green. Would appreciate a re-review when you get a chance! |
|
@Keith-CY 三个 blocking findings 都在后续提交里修复了,请 re-review 🙏 Finding 1 (model override not wired into runtime): Finding 2 (session usage never recorded): Finding 3 (hardcoded sessionId/model in Doctor.tsx): CI is green (both |
|
Hey @Keith-CY — all three blocking findings from your review have been addressed in the follow-up commits:
CI is green (frontend + rust both pass). Re-review requested — would appreciate another look when you get a chance. |
Keith-CY
left a comment
There was a problem hiding this comment.
Findings (blocking)
- Streaming runtime still ignores per-session model override
- Severity: Blocking
- File:
src-tauri/src/runtime/zeroclaw/process.rs(run_zeroclaw_message_streamingand its call path) - The new session override lookup is wired into
run_zeroclaw_message, butrun_zeroclaw_message_streamingstill reads onlyload_zeroclaw_model_preference(). Doctor + chat streaming paths call the streaming adapter (ZeroclawDoctorAdapter::start_streaming/send_streaming) which delegates torun_zeroclaw_message_streaming, so the UIModelSwitcherstill won’t affect actual runtime model for real Doctor usage.
- Streaming runtime still writes usage to global counters only
- Severity: Blocking
- File:
src-tauri/src/runtime/zeroclaw/process.rs(stream_once/ streaming path) record_session_usage()is added and used in non-streaming execution, but streaming path keepsrecord_zeroclaw_usage()only (and trace fallback updatesusage_storeonly). Doctor sessions are streaming, soget_session_usage_stats(sessionId)can stay zero while tokens are actually consumed.
- Cost estimate may show wrong model
- Severity: Medium
- File:
src/pages/Doctor.tsx+src/components/TokenBadge.tsx TokenBadgereceivesmodel={runtimeModel}fromget_zeroclaw_runtime_target()while session override can change model viaModelSwitcher. With overrides, estimated cost may be calculated against stale global runtime model instead of active per-session model.
Please re-apply the override+recording logic to the streaming path (run_zeroclaw_message_streaming + stream_once), and pass the effective session model into TokenBadge.
Finding 3: TokenBadge was using the global runtimeModel for cost estimation, ignoring per-session model overrides set via ModelSwitcher. Changes: - Doctor.tsx tracks sessionModelOverride state, derives effectiveModel (override ?? runtimeModel), passes it to TokenBadge - ModelSwitcher accepts onModelChange callback to notify parent when the user switches model or clears override - TokenBadge now receives the effective model so cost estimates match the actual model being used for the session Note: Findings 1 & 2 (streaming runtime override + usage) reference run_zeroclaw_message_streaming / stream_once which exist only in PR #34 (feat/zeroclaw-streaming-response), not in this branch's base. Will wire session override + usage into the streaming path when #34 merges.
Re-review response (10:08 UTC findings)Finding 3 (cost estimate may show wrong model) — ✅ Fixed
Finding 1 & 2 (streaming runtime ignores override + doesn't record session usage) — The functions mentioned ( Plan: When #34 merges to main, I'll rebase this PR and wire the session override + per-session usage recording into the streaming path ( |
dev01lay2
left a comment
There was a problem hiding this comment.
Addressed finding #3 — effective model for TokenBadge
Pushed 6522b01: Doctor.tsx now tracks the session model override via onModelChange from ModelSwitcher and derives effectiveModel = sessionModelOverride ?? runtimeModel, which is passed to TokenBadge. Cost estimates will now match the actual per-session model.
Findings #1 & #2 — streaming path
run_zeroclaw_message_streaming and stream_once don't exist in this branch — they're introduced by PR #34 (feat/zeroclaw-streaming-response). The non-streaming execution path (run_zeroclaw_message) already has session override lookup and per-session usage recording wired in.
Once #34 merges, I'll rebase this branch and wire the session override + usage recording into the streaming path too. Happy to handle that as a follow-up commit here or in a separate PR — whatever you prefer.
(CI failure on latest push is a flaky Text file busy in an unrelated test — re-triggered.)
Keith-CY
left a comment
There was a problem hiding this comment.
Re-reviewed PR #35 on latest HEAD. The previous three regressions are now addressed: session override is consulted in runtime execution, per-session usage is recorded in run_zeroclaw_message, and Doctor now passes effective session model (override -> runtime) into TokenBadge. No remaining blocking issues found in this revision.
Closes #32
Changes
Backend
get_session_usage_statsTauri commandestimate_query_costTauri commandFrontend