The forced 1-hour cache_control TTL (schema v14) was a mistake. Injected
breakpoints return to Anthropic's standard 5-minute TTL, caller-owned
markers are preserved verbatim instead of having their TTLs rewritten,
and the 5m/1h cache-write buckets are removed from usage parsing and
pricing (back to the single aggregate cache-creation rate). The cache
TTL selector is removed from the rectifier settings panel along with its
i18n keys in all four locales.
SCHEMA_VERSION returns to 13: the unreleased cache_creation_1h_tokens
column and the v13->v14 migration are removed. The feature never shipped
in a release and the introducing commit was never pushed, so no
databases were stamped v14 outside this machine (local DB verified at
user_version 13).
- Add the bare gpt-5.6 row (official Sol alias) plus effort-suffix variants
mirroring the gpt-5.5 accounting shape, priced at Sol rates
- The GPT-5.6 family charges cache writes at 1.25x the uncached input rate
(new for OpenAI; earlier GPT models keep their correct zero): set
6.25 / 3.125 / 1.25 for Sol / Terra / Luna across seed rows
- Repair earlier zero-value gpt-5.6 tier seeds in existing databases,
matching only untouched rows so user-customized prices survive
Let users switch between the built-in OpenAI provider and third-party Codex providers directly from the provider panel while takeover mode remains active.
Centralize the frontend capability predicate so only the fixed codex-official seed receives native-login routing support, and keep copied UUID-based official entries clearly marked as unsupported.
Add an idempotent backend command that recreates the deleted official seed, wire it into the add-provider flow, refresh localized guidance, and add mutation and provider-action regression coverage.
Allow the built-in Codex official provider to participate in takeover mode while preserving Codex's native OAuth or API-key credentials instead of persisting them into provider records.
Project official routing into a dedicated TOML provider, normalize inline tables, clean stale managed placeholders, and fail closed when the live configuration cannot be transformed safely.
Validate forwarded authorization, make official 401/403 responses non-retryable, avoid circuit-breaker pollution, and share the first-party ChatGPT endpoint across the Codex and Claude adapters.
Parse and retain Anthropic's ephemeral 5-minute and 1-hour cache-creation token buckets while preserving the existing aggregate cache-write metric for compatibility.
Price 1-hour writes at the documented premium relative to the configured 5-minute write rate, clamp inconsistent provider details safely, and include TTL buckets in usage diagnostics.
Persist 1-hour cache-write tokens with schema version 14 so zero-cost backfills and later pricing updates retain the original TTL semantics. Keep session import paths compatible through zero-valued detail fields.
Parse cache_write_tokens from OpenAI usage details and preserve cache creation data across Chat, Responses, and Anthropic conversion paths.
Add explicit input-token semantics to request logs and rollups so legacy rows subtract cache reads only while new total-inclusive rows subtract both cache reads and writes. Migrate v12 databases, normalize rollups to fresh input, and cover historical backfill behavior with regression tests.
Add Hunyuan Hy3 (released 2026-07-06, 256K context) to seed_model_pricing
under both the hunyuan-hy3 and hy3 ids, since the upstream billing id isn't
yet confirmed and one of the two should match logged usage.
Prices come from Tencent's launch-day list price (CNY 1 / 4 / 0.25 per 1M
input / output / cache-hit), converted at 1 USD ~= 7.14: 0.14 / 0.56 / 0.035.
cache_write is "0" (no published write rate). Hy3 actually uses input-length
tiered billing (<16K / 16-32K / >=32K); the single-price table holds the
lowest tier, so long-context requests are under-billed until this is revisited
against the official billing page. New model, so seed only.
Add the three GPT-5.6 tiers to seed_model_pricing, cross-checked against
OpenAI's official pricing page and OpenRouter:
gpt-5.6-sol 5 / 30 / 0.50 (cache read)
gpt-5.6-terra 2.50 / 15 / 0.25
gpt-5.6-luna 1 / 6 / 0.10
cache_write is "0": OpenAI publishes only a cached-input (read) rate and no
separate cache-write price, matching the gpt-5.x family convention. Base ids
only for now — effort-suffix variants are deferred until the upstream logging
suffix set for 5.6 is confirmed. New model, so seed only (no repair branch,
no SCHEMA_VERSION bump).
Projects are now global shared entities; Claude and Codex groups switch
independently via scoped current pointers and scoped payload slots.
- Remove scope column from profiles; keep current_profile_id_<scope>
- Use Option<Vec<String>> for mcp/skills to distinguish 'never captured'
from 'captured empty', preventing cross-side accidental disable
- Update/apply operations scoped to the active group via merge_scope_from
- Tray menu nests same shared list under Claude Code/Codex groups
- Add i18n for per-scope tooltips and 'not saved for this side' hint
Refs: profile P1 shared-entity redesign
Add a profile feature that captures the current provider, MCP,
skills and prompt state for Claude Code and Codex as a named
snapshot, and re-applies it in one click from the header switcher
or the tray Projects submenu.
- New profiles table (schema v12) with current marker in settings
- ProfileService orchestrates the four existing switch primitives
(provider first, then MCP diff, skills diff, prompt enable)
- Best-effort apply: dangling references become warnings, no rollback
- Header combobox switcher + snapshot-style manage dialog
- Tray Projects submenu shared with the UI apply/event pipeline
- i18n for zh/en/ja/zh-TW under the new profiles domain
- Integration tests covering roundtrip, dangling refs and clear
Seed claude-sonnet-5 at Anthropic list price ($3/$15 in/out, $0.30/$3.75 cache read/write per Mtok, matching Sonnet 4.6). The introductory $2/$10 promo through 2026-08-31 is intentionally not seeded. Applied on next app start via ensure_model_pricing_seeded; no SCHEMA_VERSION bump.
* feat(db): in-app recovery screen with upgrade button when DB version is too new
When the SQLite user_version is newer than the app supports (SCHEMA_VERSION),
Database::init() fails and previously dead-ended in a native Retry/Exit dialog
(Retry just fails again). The app now boots a dedicated recovery screen instead.
- Detect the recoverable "version too new" case and surface it via init_status
(kind="db_version_too_new" + db_version/supported_version); force-show the main
window and skip normal AppState boot (recovery commands need only AppHandle).
- The recovery screen first checks for an available update:
- update available -> "Upgrade app" runs the updater (download + install +
restart) with a download progress bar.
- no update (already latest) -> warns that the DB is too new even for the latest
build (likely a third-party client), so upgrading cannot help.
- install_update_and_restart now emits `update-download-progress` events; new
`check_app_update_available` command.
- i18n: en / ja / zh / zh-TW.
Verified: pnpm typecheck + format:check + test:unit (351) green; cross-model
review of Rust correctness and recovery-mode safety (no AppState panic).
* fix(db): pre-check too-new DB before schema writes; exit on close in recovery mode
Addresses review feedback on the too-new-DB recovery flow.
- P1: stored_user_version_exceeds_supported() is now checked BEFORE
Database::init(), so create_tables()'s DDL (incl. the unconditional
DROP INDEX/DROP TABLE IF EXISTS failover_queue) never runs against a
database whose user_version we cannot understand. The earlier
post-init-failure recovery branch is removed; the pre-check is the
single authoritative guard, so "don't write to a DB we can't read"
now holds from the very first DB access.
- P2: the window CloseRequested handler now detects recovery mode
(init_error kind = db_version_too_new) and exits the app instead of
honoring minimize_to_tray_on_close. Recovery mode returns before the
tray is created, so hiding the window would leave the app running with
no tray to bring it back; native-close now quits cleanly.
Verified: cargo fmt --check clean; cross-model Rust review of both fixes
(compile-correctness + no normal-startup/close regression).
Add kimi-k2.7-code pricing seed (in $0.95 / out $4.00 / cache-read $0.19
per 1M tokens, 256K context) and point all six official Moonshot Kimi
presets (Claude Code, Codex, Claude Desktop, Hermes, OpenCode, OpenClaw)
at the new model. Rename the version-tagged OpenCode/OpenClaw presets to
"Kimi K2.7 Code" and correct the OpenClaw context window to 262144.
The seed is applied via the idempotent INSERT OR IGNORE path that runs on
every startup, so existing users pick up the new pricing without a schema
migration. Kimi For Coding and Nvidia presets are intentionally untouched.
Insert a new 'claude-mythos-5' model tuple into src-tauri/src/database/schema.rs. The tuple ("claude-mythos-5", "Claude Mythos 5", "10", "50", "1.00", "12.50") is added to the models list (placed before the Claude 4.8 series) to register the Mythos 5 model with the Database schema.
- proxy_request_logs: add pricing_model column recording the basis actually
used at write time (NULL = pre-v11 rows, '' = unpriced error rows)
- cost backfill recomputes strictly by the persisted basis; the
request_model fallback now only applies to placeholder models, so
real-but-unpriced takeover rows stay at zero cost until pricing is
added instead of being permanently frozen at the alias's price
- backfill_missing_usage_costs_for_model can locate rows by pricing_model
- usage_daily_rollups: rebuild with request_model + pricing_model in the
primary key so the alias-to-real-model mapping and the pricing basis
survive the 30-day prune; legacy rows migrate with ''
- rollup_and_prune backfills costs before pruning: prune is irreversible
and used to run before the startup backfill, permanently booking
then-unpriced rows as zero
- get_model_stats groups by the effective pricing model
(COALESCE(NULLIF(pricing_model,''), model)) so costs aggregate under
the model whose prices produced them; response-mode behavior unchanged
* Add S3 Cloud Sync design document
Design for adding AWS S3 as a new Cloud Sync backend alongside WebDAV.
Hybrid approach: extract shared sync protocol, add independent S3 transport.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add S3 cloud sync implementation design (reqwest + Sig V4)
Updated design based on 2026-03-06 draft: switches from rust-s3 crate
to hand-rolled AWS Sig V4 on existing reqwest for broader S3-compatible
service support (AWS, MinIO, R2, Alibaba OSS, Tencent COS, Huawei OBS).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add S3 cloud sync implementation plan (11 tasks, TDD)
Detailed step-by-step plan covering: sync_protocol extraction, S3 Sig V4
transport, settings, sync/auto-sync modules, Tauri commands, frontend
presets/dynamic form, and i18n.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* deps: add hmac crate for S3 Sig V4 signing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract sync_protocol.rs from webdav_sync.rs for shared use
Move transport-agnostic sync protocol logic (constants, types, snapshot
building, manifest validation, artifact verification, snapshot application,
utilities) into a new shared sync_protocol module so both WebDAV and the
upcoming S3 transport can reuse it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use transport-neutral error keys in sync_protocol
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add S3 transport layer with AWS Sig V4 signing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add S3SyncSettings to AppSettings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add S3 sync module with upload/download/fetch
Implements the S3 sync protocol layer (s3_sync.rs) that combines the
shared sync_protocol with the S3 transport. Mirrors the WebDAV sync
module structure with independent sync mutex, connection check,
upload, download, fetch_remote_info, and sync status persistence.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add S3 auto sync worker with debounce
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add S3 sync Tauri commands and auto sync worker startup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add S3 sync TypeScript types and API layer
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add S3 sync i18n translations (en/zh/ja)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add S3 sync presets and dynamic form to sync settings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: preserve HTTP scheme for S3 custom endpoints (MinIO support)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add live S3 integration tests (env-var driven, --ignored)
Run with: S3_TEST_AK=... S3_TEST_SK=... S3_TEST_BUCKET=... cargo test --lib services::s3::integration_tests -- --ignored
Verifies test_connection, put_object, get_object, head_object, and 404
handling against a real S3 bucket using the project's own Sig V4 signing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: remove internal design docs before PR
* fix: wire S3 auto-sync to DB hook & sync UI state on async load
- P1: Add s3_auto_sync::notify_db_changed call in SQLite update_hook
so S3 auto-sync worker receives DB change signals (was only wired
for WebDAV, leaving S3 worker idle)
- P2: Add useEffect to update syncType selector when s3Config loads
asynchronously, preventing stale "webdav" default for S3 users
* fix: satisfy clippy for s3 sync
* fix: address s3 sync review feedback
---------
Co-authored-by: Keith (via OpenClaw) <keithyt06@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Jason <farion1231@gmail.com>
Bump default model names project-wide: gpt-5.4 -> gpt-5.5,
gemini-3.x -> gemini-3.5-flash, glm-5 -> glm-5.1, and
grok-code-fast-1 -> grok-build-0.1 across all provider presets
(claude, codex, gemini, hermes, openclaw, opencode, universal),
Gemini config, and stream check defaults.
Pricing:
- Seed gemini-3.5-flash, gemini-3.1-flash-lite, step-3.5-flash-2603,
doubao-seed-2.0-code, mimo-v2.5(/pro), qwen3-coder-480b, grok-build-0.1.
- Correct deepseek-v4-flash/pro, glm-5/5.1, grok pricing.
- Add repair_current_model_pricing: idempotent pass that fixes only
rows still equal to the outdated built-in values, preserving any
user-customized prices (seed uses INSERT OR IGNORE and cannot update
existing rows).
Fixes from review:
- opencode: drop duplicate gemini-3.5-flash variant (unreachable via
.find), keep the entry with the full minimal/low/medium/high set.
- Align stale display names/costs to gemini-3.5-flash (hermes, openclaw,
opencode); openclaw cost -> {1.5, 9, 0.15} to match seed.
- i18n (zh/en/ja/zh-TW): refresh OMO category tooltips for new model
names; fix writing tooltip to Kimi K2.5 to match its recommended.
Update tests accordingly and add a regression test asserting unique
model ids in the Google opencode preset variants.
Bump the default Opus route/model from claude-opus-4-7 to claude-opus-4-8
across provider presets (claude, claudeDesktop, hermes, openclaw, opencode,
universal), i18n locales (zh/en/ja/zh-TW), pricing seed data, and the
user-manual docs.
- Add claude-opus-4-8 pricing row ($5/$25/$0.50/$6.25); keep the 4-7 row
for historical usage stats (seeded via INSERT OR IGNORE).
- Claude Desktop proxy: accept bidirectional opus 4-7 <-> 4-8 route alias
during rollout so previously saved routes keep resolving.
- thinking_optimizer: route opus-4-8 through adaptive thinking and normalize
dotted model ids (also fixes dotted 4-6/4-7 falling back to legacy).
- usage_stats: normalize Bedrock/Vertex/aggregator opus-4-8 ids to base
pricing.
Also merge role:"system" messages into the Gemini systemInstruction in the
Anthropic->Gemini transform.
The usage dashboard previously only refreshed on app restart for users
who don't route through the cc-switch proxy. Two issues were involved:
1. The session-sync background task panicked when a Codex model name
contained non-ASCII characters (e.g. `【官】glm-5.1`), because
`normalize_codex_model` sliced `&name[name.len() - 11..]` without
verifying char boundaries. Once the task panicked, no session logs
were imported until the app was restarted (where startup-time
`rollup_and_prune` happened to flush pending data).
2. Even with sync working, the dashboard only polled every 30s and
skipped polling when the window was unfocused, so freshly-imported
data was invisible until the next poll or window refocus.
Fixes
-----
* `normalize_codex_model`: guard the 11-byte ISO-date suffix slice with
`is_char_boundary` + `is_ascii` checks. ASCII-only suffix means the
date-stripping logic is correct, and non-ASCII names (which can never
be valid date suffixes anyway) now bypass the slice safely.
* New `usage_events` module that emits `usage-log-recorded` to the
frontend whenever `proxy_request_logs` actually gains a new row.
Sources covered: proxy `log_request`, Claude/Codex/Gemini session
sync, and startup `rollup_and_prune`. Notifications use a global
`OnceLock<AppHandle>` so call sites that don't already hold an
`AppHandle` (e.g. `UsageLogger`) can notify without signature churn.
* 200ms debounce in `notify_log_recorded` collapses bursts (a single
Codex sync importing 3000+ entries triggers ~2 emits, not 3000) so
the frontend's `invalidateQueries` is never spammed.
* Frontend `useUsageEventBridge` listens for the event and invalidates
`usageKeys.all`. Hook is mounted only on `UsageDashboard`, so the
listener is unsubscribed automatically when the user navigates away.
Verification
------------
* `cargo check` passes (existing 25 dead-code warnings in
`commands/misc.rs` are pre-existing and unrelated).
* `tsc --noEmit` passes.
* Manually verified end-to-end: a Codex sync run that imported 3145
entries produced 2 debounced emits, both logged as `emit
usage-log-recorded 成功`, and the dashboard updated within ~200ms.
Behaviour notes
---------------
* `INSERT OR IGNORE` paths (Claude/Codex session sync) only notify when
the row is actually inserted, so dedup-skipped writes don't trigger
empty refreshes.
* Gemini's `INSERT … ON CONFLICT … DO UPDATE` path reuses the existing
`conn.changes() > 0` check and only notifies when token counts truly
changed.
* `rollup_and_prune` notifies once per pruning cycle (at most once per
app start) so the dashboard reflects the new aggregate state.
Co-authored-by: in30mn1a <in30mn1a@users.noreply.github.com>
Codex filters resume history by `model_provider`, so switching between
provider-specific ids like `rightcode` and `aihubmix` made past sessions
appear to vanish. Collapse all third-party providers into a single
stable bucket so cross-switch history stays visible.
- Normalize live `model_provider` to "custom" on every Codex write
(reserved built-in ids like openai/ollama are preserved).
- Add device-level one-shot migration that rewrites historical JSONL
session files and the `state_5.sqlite` threads table from legacy
provider ids into the "custom" bucket. Backs up originals under
`~/.cc-switch/backups/codex-history-provider-migration-v1/` and uses
the SQLite Backup API for the state DB.
- Record completion in `settings.json` under `localMigrations` so the
migration is strictly idempotent across launches.
- Update Codex provider preset templates to emit `model_provider = "custom"`
out of the box.
* model pricing routing: extend prefix-match families (gpt-/o1-o5/
gemini-/deepseek-/qwen-/glm-/kimi-/minimax-) with per-family dash
thresholds so short base IDs like gpt-5 no longer mis-match
gpt-5-mini; strip ISO and 8-digit date suffixes via UTF-8-safe
byte matching so claude-haiku-4-5-20251001 falls back to
claude-haiku-4-5 pricing
* SSE collector: SseUsageFinishGuard (RAII) guarantees finish() on
early return or panic; AtomicBool fast path lets push() skip the
Mutex once first-event time is recorded
* validation: shared validate_cost_multiplier / validate_pricing_source
helpers across DAO and service layers; PRICING_SOURCE_RESPONSE /
PRICING_SOURCE_REQUEST constants replace string literals; price
fields in update_model_pricing now reject empty / non-decimal /
negative input before INSERT
* backfill: add backfill_missing_usage_costs_for_model so a single
price edit only scans matching rows instead of the full log table;
startup backfill remains full-scan
* session_usage{,_codex,_gemini}: share find_model_pricing helper from
usage_stats; metadata_modified_nanos centralizes mtime precision
* frontend: NON_NEGATIVE_DECIMAL_REGEX + isNonNegativeDecimalString
replace three copies of the same multiplier regex; isUnpricedUsage
surfaces zero-cost rows that have usage tokens (cached per row to
avoid double evaluation); invalidate usageKeys.all on pricing mutate
so backfilled rows refresh
- Derive route keys from the upstream model name (pass-through style)
instead of fixed Claude aliases, and translate the legacy [1M] suffix
into the supports1m field at the import boundary. Three Claude aliases
mapped to the same upstream now collapse to a single route (e.g.
MiniMax-M2 across SONNET/OPUS/HAIKU env produces one
claude-MiniMax-M2 -> MiniMax-M2 row), with [1M] OR-aggregated.
- Add an import-time safety net that rebuilds claude-desktop-official
when missing, so users who deleted it can recover via the normal
import button without losing customizations on other providers.
- Hide API key and endpoint URL inputs in the official provider edit
form to mirror Claude Code's behavior and prevent user confusion.
- Reword the empty-state import button label for clarity.
- Apply rustfmt diffs in claude_desktop_config.rs
- Allow needless_return on current_platform_paths (cfg-mirrored arms)
- Allow too_many_arguments on RequestForwarder::forward
- Replace `let mut + reassign` with struct literals in tests
(settings, backup, provider, response_processor)
- Use Path::new instead of PathBuf::from to fix cmp_owned in misc tests
- Replace 3.14 with 3.5 in config test to avoid approx_constant lint
Adds a new ClaudeDesktop AppType that writes Claude Desktop's third-party
inference profile under configLibrary/, sharing _meta.json with other
launchers (Ollama-compatible) so cc-switch can coexist with them.
Two switch modes:
- direct: provider already exposes claude-* / anthropic/claude-* model
ids on Anthropic Messages, Claude Desktop connects to it directly.
- proxy: cc-switch's local proxy acts as the inference gateway,
presenting only claude-* route names to Claude Desktop and mapping
them to real upstream models. Required after Anthropic restricted
Claude Desktop to claude-family ids.
Backend:
- New module claude_desktop_config with snapshot/rollback, official seed
bypass, /claude-desktop/v1/{models,messages} routes, and a single
source of truth for default proxy routes.
- Gateway token persisted in SQLite, validated on every proxied request.
- get_claude_desktop_status surfaces drift signals (stale models,
missing routes, proxy stopped, base URL mismatch, missing token).
Frontend:
- Slim ClaudeDesktopProviderForm independent from ProviderForm,
controlled by a top-level appId guard.
- ProviderList banner consumes the status query (5s polling) and
renders actionable diagnostics.
- ClaudeDesktopRouteToggle in the header to start/stop the local
gateway without touching takeover state.
- Three-locale i18n synchronised.
Hermes:
- Parse ~/.hermes/state.db sessions (incl. profiles/*/state.db) into
proxy_request_logs with data_source='hermes_session', WAL-aware
incremental sync, Hermes-reported cost preferred over model_pricing
fallback
Zero-cost bug (dashboard showed \$0 totals):
- GPT-5.5 family default pricing (~83% of affected rows used GPT-5.5)
- find_model_pricing_row: ASCII-lowercase normalization so
"OpenAI/GPT-5.5@HIGH" matches seeded "gpt-5.5"
- Startup cost backfill in async task: scan rows where total_cost <= 0
but tokens > 0, recompute via model_pricing in a single transaction
Performance:
- Add (app_type, created_at DESC) covering index for dashboard range
queries
- Add expression index on COALESCE(data_source, 'proxy') so dedup EXISTS
subqueries use index lookup instead of full scan; drop superseded
idx_request_logs_dedup_lookup
Refactor:
- row_to_request_log_detail helper (3-way de-dup; fixes cost_multiplier
\"1\" vs \"1.0\" drift between callers)
- Promote get_sync_state/update_sync_state to shared session_usage
module (4 copies -> 1)
- run_step helper in lib.rs replaces 9 if-let-Err blocks
- maybe_backfill_log_costs returns bool to skip duplicate total_cost
parsing in caller
Proxy writes and session-log sync wrote to proxy_request_logs with
mismatched request_ids: only Claude on a native Anthropic backend used the
shared `session:{message_id}` key. Codex/Gemini and Claude-through-OpenAI
providers always produced distinct ids, so primary-key dedup never fired
and every real request was recorded twice.
Adds a 7-dim fingerprint dedup (app_type, 4 token counts, 2xx status,
model with case-insensitive match, ±10min window) wired into three layers:
- Write path: should_skip_session_insert() blocks duplicate session rows
before INSERT, unifying the previously-divergent Claude/Codex/Gemini
paths through a single DedupKey-based helper.
- Read path: effective_usage_log_filter() excludes already-covered session
rows from every aggregation query.
- Rollup path: same filter applied so usage_daily_rollups never absorbs
duplicates.
Also adds a covering index (idx_request_logs_dedup_lookup) so the EXISTS
subquery stays index-only, and a transform.rs regression test that pins
openai_to_anthropic id preservation - the missing piece that lets
Claude+OpenAI-compatible providers reuse the session: id scheme.
DeepSeek released V4 flash/pro; legacy IDs deepseek-chat / deepseek-reasoner
now alias to deepseek-v4-flash and will be deprecated.
- Update claude/hermes/opencode/openclaw presets to v4-pro / v4-flash,
context 128K -> 1M; Claude Anthropic-compat endpoint routes OPUS/SONNET
to v4-pro and HAIKU to v4-flash, plus an explicit modelsUrl override.
- Seed deepseek-v4-flash ($0.14/$0.28 per 1M) and deepseek-v4-pro
($1.68/$3.36 per 1M) into model_pricing; older v3.x / chat / reasoner
rows kept for historical usage stats (INSERT OR IGNORE).
- Refresh user-manual (zh/en/ja) pricing table and note that legacy model
IDs are billed at v4-flash rates.
Moonshot's official USD pricing for kimi-k2.6 is $0.95 input /
$4.00 output / $0.16 cache-hit per 1M tokens (~58-60% higher than
K2.5). The previous commit copied K2.5's $0.60/$2.50/$0.10, which
would have under-billed K2.6 traffic in the usage dashboard.
No migration needed since this version is unreleased; INSERT OR
IGNORE will write the correct values on first launch.
Bump model id and display name from K2.5 to K2.6 in Hermes, OpenClaw,
OpenCode, and Claude (direct api.moonshot.cn) presets. Pricing,
context window, and base URL are unchanged.
Add kimi-k2.6 row to model_pricing seed; no migration needed since
seed_model_pricing uses INSERT OR IGNORE and runs on every startup
via ensure_model_pricing_seeded. Old kimi-k2.5 row is kept to
preserve historical usage stats.
Nvidia aggregator forwards (moonshotai/kimi-k2.5) intentionally keep
the K2.5 SKU until Nvidia's catalog confirms K2.6.
Wire hermes through SkillApps struct, DAO SQL, command parser, and
SKILLS_APP_IDS. Add a Skills entry to the Hermes toolbar. Simplify
skill_sync test fixtures to use SkillApps::default().
- Seed claude-opus-4-7 pricing (same tier as 4.6: $5 / $25 / $0.50 /
$6.25 per million tokens). Relies on incremental INSERT OR IGNORE
seeding; no SCHEMA_VERSION bump needed.
- Whitelist opus-4-7 in thinking optimizer so it uses adaptive
thinking + max effort + 1M context beta, matching 4.6 behavior.
- Bump default OPUS model in PIPELLM and AWS Bedrock (AKSK / API Key)
presets to 4.7. Bedrock SKU drops the -v1 suffix per the official
4.7 model card (anthropic.claude-opus-4-7 and
global.anthropic.claude-opus-4-7).
- Bump SCHEMA_VERSION from 9 to 10
- Add enabled_hermes column to mcp_servers and skills tables
- Add migrate_v9_to_v10 with table_exists guard for skills (may not
exist in databases migrated from very old versions)
- Update dao/mcp.rs to fully read/write enabled_hermes in all queries
- Update dao/skills.rs: don't SELECT enabled_hermes (Hermes doesn't
support Skills yet), keep column indices clean
Register AppType::Hermes across the entire Rust backend:
- Add Hermes variant to AppType enum with additive mode and MCP support
- Add hermes field to McpApps, SkillApps, CommonConfigSnippets, and all
per-app structs (McpRoot, PromptRoot, VisibleApps, AppSettings)
- Create minimal hermes_config.rs with get_hermes_dir() respecting
settings override, matching the pattern of other app config modules
- Update all match arms in commands, services, deeplink, proxy, mcp,
session_manager, and test files
- Extract shared build_additive_app_settings() to eliminate duplication
between OpenClaw and Hermes deep link handling
- Combine identical OpenClaw/Hermes proxy match arms into unified arms
* feat(usage): enhance usage stats backend and query hooks
* feat(usage): redesign calendar date range picker with auto-switch and simplified layout
* refactor(usage): streamline dashboard layout and stats components
* refactor(usage): compact request log table with merged cache/multiplier columns and centered layout
* feat(i18n): add cache short labels and usage stats translations for zh/en/ja
* Align usage dashboard stats with range boundaries
The usage dashboard mixed second-precision detail rows with day-level rollups, which caused custom half-day ranges to overcount historical rollup data and left the request log paginator on stale pages after top-level filter changes.
This change limits rollups to fully covered local days, aligns multi-day trend buckets with natural local days, and resets request log pagination when the dashboard range or app filter changes.
Constraint: usage_daily_rollups stores only daily aggregates after pruning old detail rows
Rejected: Include partial boundary rollups proportionally | historical intra-day detail is unavailable after pruning
Rejected: Force RequestLogTable remount on range change | would discard local draft filters unnecessarily
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep summary, trends, provider stats, and model stats on the same rollup-boundary rules
Tested: cargo test --manifest-path src-tauri/Cargo.toml usage_stats
Tested: pnpm exec vitest run tests/components/RequestLogTable.test.tsx
Tested: pnpm typecheck
Not-tested: Manual UI validation in the Tauri app
* Preserve full-day usage filters at minute precision
The latest review surfaced two interaction bugs in the usage dashboard: rollup-backed stats undercounted end days selected via the minute-precision picker, and immediate select changes accidentally applied unsubmitted text drafts from the request log filters.
This change treats 23:59 as a fully selected local end day for rollup inclusion and narrows select-side state syncing so app/status updates do not commit provider/model drafts.
Constraint: The custom range picker emits minute-precision timestamps, while rollups are stored at day granularity
Rejected: Require exact 23:59:59 end timestamps | unreachable from the current picker UI
Rejected: Rebuild applied filters from the full draft state on select changes | silently commits unsaved text input
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep request-log text fields on explicit apply semantics even when select filters remain immediate
Tested: cargo test --manifest-path src-tauri/Cargo.toml usage_stats
Tested: pnpm exec vitest run tests/components/RequestLogTable.test.tsx
Tested: pnpm typecheck
Not-tested: Manual Tauri dashboard interaction
* refactor(usage): move range presets into date picker, single-row layout
- UsageDateRangePicker: add preset shortcuts (今天/1d/7d/14d/30d) inside
popover top; clicking a preset applies immediately and closes popover
- UsageDashboard: collapse to single row (app filters + refresh + picker);
remove standalone preset buttons and summary stats bar
- RequestLogTable: replace static Calendar badge with interactive
UsageDateRangePicker via onRangeChange prop; single filter row
* Keep usage pagination regression coverage aligned with the rendered UI
The new regression test was asserting a non-existent pagination label and page summary text, so it failed before it could verify the real page-reset behavior. This commit switches the assertions to the numbered pagination buttons that the component actually renders and validates the reset through the query hook arguments.
Constraint: RequestLogTable exposes numbered pagination buttons, not a "Next page" label or "2 / 6" summary text
Rejected: Add synthetic pagination labels solely for the test | would couple production markup to a test-only assumption
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Prefer pagination assertions that follow the rendered controls or hook inputs instead of invented summary text
Tested: pnpm vitest run tests/components/RequestLogTable.test.tsx; pnpm typecheck; pnpm test:unit
* refactor(usage): clean up dead code and polish date range picker
- Remove unused exports MAX_CUSTOM_USAGE_RANGE_SECONDS,
timestampToLocalDatetime, and localDatetimeToTimestamp from
usageRange.ts (replaced by the calendar picker)
- Deduplicate getPresetLabel from UsageDashboard and
UsageDateRangePicker into shared getUsageRangePresetLabel helper
- Add aria-label, aria-current and aria-pressed to calendar day
buttons so screen readers can disambiguate same-numbered days
across adjacent months
- Drop unused cacheReadShort and cacheWriteShort i18n keys (zh/en/ja);
the request log table renders R/W prefixes inline
- Align customRangeHint copy with the removed 30-day limit by
dropping "up to 30 days" wording (zh/en/ja)
* fix(usage): align rollup cutoff to local midnight to keep days complete
`rollup_and_prune` previously used `Utc::now() - retain_days * 86400`
as the cutoff. Because rollups are bucketed by *local* date and detail
rows below the cutoff are pruned, an unaligned cutoff left the youngest
rolled-up day half-rolled-up and half-pruned. Combined with the new
`compute_rollup_date_bounds` boundary trimming (which excludes any
rollup day not fully covered by the requested range), custom range
queries that touch that day silently under-count summary, trend,
provider, and model stats.
Fix the invariant at the source: snap the cutoff to the next local
midnight after `(now - retain_days)`. Every rollup row now reflects a
complete local day, so the boundary trimmer's all-or-nothing assumption
holds.
Includes unit tests for the cutoff math (typical case + already-on-
midnight case). DST gap is handled defensively by bumping forward by
an hour.
Addresses Codex P2 review finding on PR #2002.
---------
Co-authored-by: Jason <farion1231@gmail.com>
Add pricing data for 4 new providers (Qwen, xAI Grok, Mistral, Cohere)
and supplement existing providers (MiniMax M2.5/M2.7, GLM-5/5.1,
Doubao Seed 2.0, MiMo V2 Pro, OpenAI o1/o3/codex-mini/gpt-5-mini/nano).
Fix outdated prices for deepseek-chat, deepseek-reasoner, and kimi-k2.5.
Fix display_name casing "Mimo" → "MiMo" for consistency.
Use prepared statement in seed_model_pricing() to avoid recompiling SQL
on each of ~130 INSERT iterations.
Schema migration v8→v9: DELETE + re-seed model_pricing for existing users.
- Make migrate_v6_to_v7 check skills table existence before ALTER
- Make migrate_v7_to_v8 check model_pricing table existence before UPDATE
- Fix SessionManagerPage test: use getByRole heading instead of getAllByText
which breaks when highlightText splits text across <mark> elements
Introduce a one-time welcome dialog that explains CC Switch's workflow
to new users: how their existing config is preserved as a "default"
provider and how the bundled "Official" preset enables one-click revert.
Upgrade users are excluded by checking is_providers_empty() at startup
and never see the dialog.
Persistence follows the existing *_confirmed convention in AppSettings
(proxy/usage/stream_check/failover), stored in settings.json. The field
is only written when the user explicitly clicks the confirm button,
keeping its semantics strictly about user acknowledgement.
Also adds two reusable DAO helpers:
- Database::is_providers_empty for fresh-install detection, using
EXISTS(SELECT 1) for a short-circuit query.
- Database::get_bool_flag accepting "true" | "1", with
init_default_official_providers migrated to use it.
Dialog copy in zh/en/ja uses conditional phrasing so it stays
accurate whether or not existing live config was found.
Drops the friction of clicking the manual "Import current config" button
for OpenCode and OpenClaw — they now match the auto-import behavior the
previous commit added for Claude/Codex/Gemini.
- New "1.6." startup block in lib.rs runs both
import_opencode_providers_from_live and import_openclaw_providers_from_live
on every launch. The functions are id-keyed and idempotent, so re-running
just picks up new providers added externally to the live JSON files.
- Both functions now use a new Database::get_provider_ids() helper
(HashSet<String> from a single SELECT id-only query) instead of
get_all_providers(), avoiding the N+1 endpoint sub-queries that would
otherwise hit the startup hot path on every launch.
New and existing users now see a built-in "Claude Official" / "OpenAI
Official" / "Google Official" entry in their provider list, so switching
back to the official endpoint is one click away instead of buried in the
README.
- New providers_seed.rs holds the three seeds (id, name, settings_config,
icon) keyed by AppType, with a single is_official_seed_id() helper that
scans OFFICIAL_SEEDS so the id list has one source of truth.
- Database::init_default_official_providers() runs once per database
(gated by an official_providers_seeded setting flag), appends each seed
to the end of the sort order, and never touches is_current.
- Startup also auto-imports the live config (settings.json / auth.json /
.env) as a "default" provider before seeding, so users with an existing
manual config don't lose it when they click the official preset.
- Database::has_non_official_seed_provider() replaces the get_all_providers
call in import_default_config's gating check with an id-only scan,
dropping the N+1 endpoint sub-queries from every startup.