During proxy takeover, switching third-party Codex providers left the
client-visible provider name stale: sync_codex_live_from_provider_while_proxy_active
based the live config on the existing live file and only patched
base_url/wire_api/model, never refreshing model_provider or
model_providers.<id>.name. The Codex app kept showing the previous
provider in its bottom-right label.
Rebuild the effective settings from the DB for the selected provider so
the live config carries the correct provider key and display name, then
merge MCP servers back from the existing live config. base_url stays
pointed at the local proxy, and official OAuth in auth.json is untouched
(takeover writes config.toml only when auth preservation is enabled).
Generalize preserve_codex_mcp_servers_in_backup ->
preserve_codex_mcp_servers_from_existing_config since it now serves both
the backup and live-sync paths.
Restyle the proxy-takeover notice in the Codex editor from the boxed
Alert to the amber inline-hint style used by the endpoint hints, and drop
implementation jargon (127.0.0.1 / PROXY_MANAGED) that users could not
interpret. The notice and the auth/config hints now simply state that the
form shows the stored provider config rather than the proxy-managed live
config. i18n synced across en/zh/ja/zh-TW.
Gate provider sync and switching on the restore backup / live placeholder
("is this live file owned by takeover?") instead of the lagging
proxy_config.enabled and proxy-running flags. The backup is created
before enabled=true is committed, so during that activation window the
old guards were blind and a concurrent sync/switch could rewrite the
taken-over live file, clearing Codex auth.json for a mis-categorized
provider.
Acquire a per-app switch lock around both set_takeover_for_app and
provider switching so the two cannot interleave, splitting the locking
entry points into outer (lock) / inner (no-lock) pairs to stay
deadlock-free. Preserve the official OAuth auth in provider-rebuilt
restore backups by routing the provider token into config.toml. Refine
takeover idempotency to require the live config to point at the current
proxy URL, rebuilding from backup when it does not.
Add unit and integration tests covering the official -> DeepSeek ->
takeover on/off lifecycle and the stopped-proxy switch path.
The reported "OAuth access token disappears when enabling Codex proxy
takeover" was a display artifact, not data loss: auth.json on disk kept
the OAuth login the whole time. During takeover the edit dialog falls
back to the stored provider config (so it does not surface the proxy
placeholder), which for a third-party provider shows that provider's own
key instead of the live auth.json, making the OAuth token look gone.
Thread an isProxyTakeover flag from App through ProviderForm into the
Codex editor and show an explicit notice plus storage-aware auth/config
hints clarifying that the form displays the stored provider config while
the live config is temporarily managed by the proxy. Drop the
proxy-running condition so the notice shows whenever takeover is active,
even with the proxy stopped.
Add a regression test asserting the dialog does not read live settings
during takeover and renders the database config. i18n synced across
en/zh/ja/zh-TW.
Preserve-mode takeover routes the PROXY_MANAGED placeholder into
config.toml and must leave auth.json (the ChatGPT OAuth login) intact.
c9cadd6e covered only the None-provider write branch; the
Some(provider) branch still ran the category-based auth decision in
codex_config::write_codex_live_for_provider, whose first clause
(category == "official" && has_login_material) ignores the preserve
flag entirely. Because takeover stamps OPENAI_API_KEY = PROXY_MANAGED
into auth, codex_auth_has_login_material returns true, so a provider
stored with a stale/mis-classified "official" category (e.g. DeepSeek)
had its real auth.json overwritten with the placeholder — the access
token vanished on takeover and reappeared on cleanup.
Fix at the takeover entry instead of patching the fragile category
logic: add write_codex_takeover_live_for_provider, which under
preservation detects the PROXY_MANAGED placeholder in auth and writes
only config.toml (bearer token + catalog projection), never touching
auth.json. Gating on the placeholder is orthogonal to category, so any
mis-classification is handled. All four takeover sites
(sync-while-active, takeover_live_configs, _strict, _best_effort) now
route through it; restore (verbatim) and non-takeover provider writes
are unchanged.
Also projects the model catalog via
prepare_codex_live_config_text_with_optional_catalog, so the
model_catalog_json pointer survives takeover too.
Add a regression test: official-category provider + preserve enabled +
proxy takeover must keep auth.json byte-identical while moving the
placeholder into config.toml.
Turning proxy takeover off restores Live from a stored backup via
write_codex_live_verbatim. That path mishandled the Codex model catalog
for two backup shapes that need opposite treatment:
- Snapshot backup (read_codex_live_settings -> {auth, config}): no inline
modelCatalog, but the config.toml text already carries the live
model_catalog_json pointer. The old code ran catalog projection, which
saw no specs and stripped the pointer.
- Provider-rebuilt backup (update_live_backup_from_provider): inline
modelCatalog (DB SSOT) with a pointer-less config text. A pure verbatim
write ignored the inline catalog and never regenerated the pointer.
The projection decision is orthogonal to auth: a provider-rebuilt backup
can pair an inline modelCatalog with empty auth.json ({}) when the API key
lives in the config's experimental_bearer_token. The empty-auth branch
raw-wrote config and skipped projection entirely, so that shape lost its
mapping too.
Decide the config.toml text once, before splitting on auth, via the new
prepare_codex_live_config_text_with_optional_catalog helper: project only
when an inline modelCatalog is present, else keep the text raw (preserving
any existing pointer). Every config-writing branch (write-auth,
delete-auth, no-auth) now applies it consistently. Add regression tests
covering all three shapes.
`modelCatalog` is a cc-switch-private field whose SSOT is the database; Live's
config.toml only carries a lossy `model_catalog_json` projection. Proxy
takeover/restore cycles and the official Codex.app rewriting config.toml can
drop that projection, so `read_live_settings` reconstructs an empty catalog.
Two paths then overwrote the stored mapping with that empty Live snapshot:
- Switch-away backfill (`switch_normal` -> `restore_live_settings_for_provider_backfill`):
now overlays the DB provider's `modelCatalog`, falling back to the
Live-reconstructed one only when the DB has none.
- Edit dialog (`EditProviderDialog`): when editing the active Codex provider it
preferred Live over the DB SSOT; now keeps the DB `modelCatalog` so opening +
saving no longer clears the mapping table.
Add Rust backfill tests (preserve DB catalog when Live lacks it; keep Live
catalog when DB has none) and a frontend regression test for the edit dialog.
* fix(codex): always update model catalog JSON on provider switch
Without this fix, the cc-switch-model-catalog.json file and the
model_catalog_json field in config.toml were only regenerated when
should_sync_backup was true (proxy active or backup exists). Switching
providers while the proxy was idle left the catalog pointing at the
previous provider's models, requiring a full restart to take effect.
After the existing should_sync_backup block, unconditionally call
write_codex_provider_live_with_catalog when switching a Codex provider
and the proxy is not currently active (live_taken_over == false). This
mirrors what live.rs already does during a normal provider apply.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(codex): refresh catalog on restored hot switch
---------
Co-authored-by: yueqi.guo <guo_yueqi@qq.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Jason <farion1231@gmail.com>
* fix(usage): resolve per-app credentials for native balance/coding-plan queries
The native usage-query paths (balance + coding_plan) in
`query_provider_usage_inner` read credentials only from `env.ANTHROPIC_*`.
That matches Claude providers, but Codex stores its key in
`auth.OPENAI_API_KEY` with the base URL inside a TOML `config` string,
Hermes/OpenClaw flatten them at the top level, and OpenCode nests them under
`options`. So the card "refresh usage" / auto-query returned empty
credentials and failed ("查询失败") for those apps, even though the
config-page "Test" button worked (the frontend extracts per-app correctly).
Introduce a single per-app resolver `Provider::resolve_usage_credentials`
that mirrors the frontend `getProviderCredentials`, and route both native
branches through it. Add `extract_codex_base_url` to `codex_config` as the
canonical Codex TOML base-URL parser and make the proxy adapter delegate to
it (removing a duplicate copy). Align the frontend `getProviderCredentials`
to cover OpenCode and Claude Desktop and to use the same OpenRouter/Google
key fallbacks as the backend.
Fixes#3158Fixes#3100Fixes#2625
* refactor(usage): explicit AppType arms + frontend trailing-slash trim
Address two review nits on the per-app credential resolver:
- provider.rs: replace the catch-all `_` arm in resolve_usage_credentials with an explicit `AppType::Claude | AppType::ClaudeDesktop` arm, so a new AppType variant fails to compile here instead of silently defaulting to the Anthropic env shape.
- UsageScriptModal.tsx: normalize getProviderCredentials baseUrl through a single trailing-slash trim so the frontend 'Test' path matches the backend resolver (trim_end_matches), keeping front/back truly in lockstep.
No behavior change for well-formed configs; tests/typecheck/fmt/clippy clean.
* fix(usage): skip empty primary credential fields in fallback chain
`obj.get(key)` returns `Some` for a present-but-empty field, so the
`.or_else()` fallback chains for the Claude/ClaudeDesktop and Gemini api-key
lookups only skipped *absent* keys, not empty ones. Presets seed fields like
`ANTHROPIC_AUTH_TOKEN` as present-but-empty placeholders, so a provider whose
real key lives in a fallback field (`ANTHROPIC_API_KEY` / `OPENROUTER_API_KEY`
/ `GOOGLE_API_KEY`, or `GOOGLE_API_KEY` for Gemini) resolved to an empty key on
the native balance/coding-plan path — while the frontend `a || b` (which skips
empty strings) still found it, reproducing the same Test-works / refresh-fails
divergence this PR removes.
Add a `first_non_empty` helper that skips present-but-empty values, matching
the frontend `||` semantics, and use it for both fallback chains. Tests cover
empty primary + populated fallback for Claude and Gemini.
* fix(codex): add multi-platform CLI discovery and static gpt-5.5 template fallback for model catalog generation
When cc-switch generates a Codex model catalog for third-party providers,
it needs the gpt-5.5 model definition as a template. Previously it relied
on either ~/.codex/models_cache.json (only exists when Codex has connected
to OpenAI) or running 'codex debug models --bundled' (fails when codex
is not on the Tauri app's PATH, common in macOS GUI environments).
This commit adds a three-tier fallback:
1. Try 'codex' from PATH, then platform-specific common paths
(/opt/homebrew/bin/codex, /usr/local/bin/codex, etc.)
2. If all CLI attempts fail, use a compile-time embedded gpt-5.5
template (extracted from codex 0.135.0 bundled models)
Also adds tests for the static template validity and CLI candidates.
* fix(codex): discover user node codex installs
---------
Co-authored-by: Jason <farion1231@gmail.com>
* fix: add kimi/moonshot to Anthropic tool thinking history normalizer
Kimi's Anthropic-compatible endpoint requires reasoning_content in
assistant tool call messages when thinking is enabled, same as DeepSeek.
The is_reasoning_content_compatible_identifier already included kimi/moonshot
but is_anthropic_tool_thinking_history_identifier was missing them, causing
400 errors on multi-turn conversations with tool calls.
Closes#3351
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: unify reasoning vendor hints into a single SSOT list
Replace the two separately-maintained vendor identifier lists with one
shared REASONING_VENDOR_HINTS constant and a single
is_reasoning_vendor_identifier predicate. The Anthropic
tool-thinking-history matcher and the openai_chat reasoning_content
matcher previously kept independent lists, which is exactly how
kimi/moonshot ended up in one but not the other (#3351). A single source
of truth keeps the two from drifting apart again.
Also add a Kimi regression test for the Anthropic tool-history
normalization path.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Jason <farion1231@gmail.com>
When "preserve official auth on switch" is enabled, proxy takeover routes
the PROXY_MANAGED placeholder into config.toml's experimental_bearer_token
and leaves auth.json (the ChatGPT OAuth login) untouched. Takeover detection
only inspected auth.json's OPENAI_API_KEY, so it never recognized this state
and returned a false negative, which led downstream paths to clobber the
preserved OAuth login.
- Detection: is_codex_live_taken_over now also matches a config.toml
experimental_bearer_token equal to the placeholder, fixing detect/cleanup/
restore/startup-recovery in one place.
- Cleanup: remove the config.toml bearer token only when it equals the
placeholder (new remove_codex_experimental_bearer_token_if predicate), so a
real third-party key is never stripped.
- Write: under preservation, the None-provider takeover path writes only
config.toml and keeps auth.json intact, matching the provider path.
- Settings: rename the section to "Codex App Enhancements" and reword the
description across all four locales.
- Add tests covering OAuth preservation on takeover and placeholder-only
cleanup.
Collapse the two duplicated write_codex_live_atomic branches in
write_codex_live_for_provider into a single should_write_auth guard.
This is behavior-preserving: `if A {X} else if B {X} else {Y}` becomes
`if A || B {X} else {Y}`.
Adapt the Codex switch tests to the new opt-in default for
preserve_codex_official_auth_on_switch (flipped off in 3f59ab37):
add an enable_codex_official_auth_preservation() test helper for the
cases that assert the auth-preserving path, and tag the official login
provider with category="official" so it routes through the official
branch rather than relying on the global preservation flag.
Add a regression test locking the default (preservation off) behavior:
switching to a third-party provider rewrites auth.json with the new
API key and discards the existing ChatGPT OAuth login. This is the
dual of the existing preserve-and-backfill test, which only covered
the opt-in path.
When forward_with_retry fails for Codex endpoints (/chat/completions, /responses, /responses/compact), the handlers previously returned the bare ProxyError, surfacing a terse body to the client. Build a richer JSON error instead: the message embeds provider, model, endpoint and upstream status, and the error object carries those as structured fields alongside a stable cc_switch_* code.
Align map_proxy_error_to_status with ProxyError::into_response so the manually built responses use the canonical status codes (AuthError->401, Config/InvalidRequest->400, TransformError->422, StreamIdleTimeout->504, AlreadyRunning->409, NotRunning->503); previously these forwarder-level errors collapsed to 500.
Flip preserve_codex_official_auth_on_switch from true to false so
third-party Codex switches overwrite auth.json by default, matching the
expectation that switching providers also swaps credentials. Users who
rely on keeping the ChatGPT login in auth.json while on a third-party
provider (for official plugins / remote login) can enable it in
Settings -> Codex Authentication.
The toggle field ships for the first time here (it is not in v3.16.0),
so no existing settings.json holds an explicit value -- every user lands
on the new default and no migration is required.
Also set the flag explicitly in the preservation unit test instead of
relying on the global default, keeping it valid now that the default is
false.
The version probe ran `cmd /C "\"{path}\" --version"` on Windows. Rust's
own arg quoting plus the manual quotes produced nested quotes that cmd
(without /S) misparsed, so it reported even working tools as an
unrecognized command -> non-zero exit -> shown as "installed but not
runnable". cmd's error is emitted in the OEM code page (e.g. GBK on
zh-CN) and was decoded as UTF-8, turning it into mojibake.
- Add decode_command_output: try strict UTF-8, then OEM (GetOEMCP), then
ANSI (GetACP) code page via MultiByteToWideChar; route all command
output decoding through it (probes, lifecycle output, osascript,
terminal launchers).
- Add run_windows_tool_version_command: run .exe directly (no cmd);
invoke .cmd/.bat via `cmd /D /S /C call <quoted> --version` with
raw_arg to bypass Rust's quoting and keep deterministic quote handling.
- Add windows-sys Win32_Globalization dependency.
Claude Desktop's 3P validation only accepts claude-{sonnet,opus,haiku}-*
role IDs, so providers must map every tier. Bring the Desktop mapping flow
in line with Claude Code and fix the fallout that broke sub-agent Haiku calls.
- Proxy form: replace the dynamic route list with fixed Sonnet/Opus/Haiku
tiers; blank tiers backfill from the first filled tier (Sonnet first) on
submit and inherit its supports1m flag
- Backend: add a role-keyword fallback in map_proxy_request_model so dated
official names (e.g. claude-haiku-4-5-20251001) resolve to the right tier,
guarded by is_claude_safe_model_id so [1m]-suffixed IDs stay rejected
- Tighten is_claude_safe_model_id / isClaudeSafeRoute to reject degenerate
role IDs like "claude-sonnet-"
- Fix the seed-effect race where normalizing empty routes to three blank
tiers blocked the default-route backfill
- Sync switch hints, placeholders, and the zh/en/ja/zh-TW locales to the
three-role-ID rule
- Update zh/en/ja user manual (2.1, 2.6), calling out legacy Claude IDs
(claude-3-5-sonnet-...) as a rejected example
Tests: 282 frontend + 34 backend claude_desktop; typecheck, clippy, fmt clean.
The websiteUrl for ShengSuanYun presets pointed at the bare domain while
only apiKeyUrl carried the from=CH_4HHXMRYF referral code. Append the same
referral param to websiteUrl across all provider presets so the in-app
'open website' jump is also attributed to the channel.
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.
When importing a Claude provider via deep link, the preview dialog
correctly shows every env field carried in the `config` payload — but
on confirmation `build_claude_settings` only persisted the standard
fields mapped from URL params (`apiKey`, `endpoint`, `model`,
`haiku/sonnet/opusModel`). Any non-standard env keys such as
`ANTHROPIC_CUSTOM_HEADERS`, `API_TIMEOUT_MS`, or
`CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS` were silently dropped,
making the preview and the actual write diverge.
Start the env map from the inline `config` payload (best-effort
decode via the new `extract_claude_config_env` helper) and then
overlay the URL-param-derived standard fields on top, so:
- URL params remain authoritative for standard fields (unchanged)
- Custom env keys present in `config` survive the import
- Deep links without a `config` payload behave exactly as before
Adds two regression tests:
- `test_build_claude_provider_preserves_custom_env_fields` proves
custom fields are kept and URL params override same-named config
fields.
- `test_build_claude_provider_without_config_unchanged` guards the
backward-compatible path (no `config` → only standard env keys).
Closes#2927
Co-authored-by: lipeng31 <lipeng31@tanyang.co>
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>
Use semver comparison instead of string inequality so locally-ahead prerelease builds aren't misreported as outdated; backend now fetches the full npm dist-tags and, when the local version leads latest, surfaces the tool's prerelease channel (claude=next) as latest.
- Always emit `model_provider = "custom"` from deep link, UniversalProvider, and the universal form modal so future writes share one stable routing key.
- Add `codex_provider_template_v1` local migration that rewrites legacy keys (aihubmix/ccswitch/...) under `[model_providers.custom]`, updates profile refs, and backs up the original settings_config under `~/.cc-switch/backups/<timestamp>/providers/`.
- Tighten history migration source detection to a whitelist plus `[model_providers.<id>]` existence check so user-authored keys are never rewritten in jsonl/state DB.
- Encode deep link name/model/endpoint through `toml_edit::Value` so display names containing quotes or backslashes no longer break the generated config.toml.
- Stabilize provider settings backup filename hash with Sha256 (was process-random SipHash).
The one-time history migration already consolidates legacy provider IDs
into a single bucket; there is no need to normalize on every write.
This preserves user-chosen provider identities through the full
write/backup/restore cycle.
The Fill Recommended button was misleading — it showed toast.success even
when most slots couldn't be filled due to model ID mismatches with the
user's configured providers.
Changes:
- Sync OMO_BUILTIN_AGENTS/CATEGORIES recommended fields with upstream
oh-my-openagent model-requirements (gpt-5.4→5.5, kimi-k2.5→claude-sonnet-4-6, etc.)
- Add toast.warning tier when unmatched >= filled, showing slot:model pairs
(e.g. "Sisyphus: claude-opus-4-7") so users know exactly what to configure
- Upgrade fillRecommendedNoMatch to also show examples
- Add fillRecommendedMostlyUnmatched i18n key (zh/en/ja/zh-TW)
Scan Codex archived_sessions alongside active sessions so archived conversations appear in the session manager.
Allow deletion from any validated Codex session root while keeping path safety checks in place.
* Add Traditional Chinese localization
* fix: address zh-TW formatting and token units
- Format `zh-TW.json` with Prettier.
- Use Traditional Chinese `萬` and `億` units for zh-TW token summaries.
- Add usage formatting coverage for Traditional Chinese locale aliases.
---------
Co-authored-by: Jason <farion1231@gmail.com>