Commit Graph
100 Commits
Author SHA1 Message Date
Jason 370d089be8 fix(prompt): only clear the prompt file when the last enabled prompt is disabled
Saving any disabled prompt emptied the app's prompt file whenever no prompt
was enabled, so adding or importing a prompt wiped a hand-written AGENTS.md
or Hermes SOUL.md without a backup. A deeplink import also lost the file
before enable_prompt could back it up.

Follow the MiniMax Code branch: read the previous row before saving and
clear the file only when this prompt was enabled, is now disabled, and no
other prompt is enabled.
2026-10-01 23:20:06 +08:00
Jason 40d3420f25 fix(providers): prefer canonical pointers in the vendor fallback
A vendor that also hosts other vendors' models (NVIDIA lists
minimaxai/minimax-m2.7 without a canonical id) was counted as a second
vendor next to the entry relays point at, so the fallback gave up and
models like MiniMax-M2.7, gpt-5.4 and kimi-k2.6 got no metadata through
unrecognized relays. Use the entries relays point at first and only fall
back to a vendor's own entry when nothing points anywhere.
2026-10-01 23:20:06 +08:00
Jason e675d586a0 refactor(providers): share models.dev query options and committable refs
- Export modelsDevQueryOptions so the metadata fill hook and both usage
  pricing dialogs use one query definition, including the one-hour
  gcTime the fill hook needs for its prefetch-only cache.
- Add useCommittableRef for the "latest value + synchronous commit"
  pattern repeated in the Codex, Hermes, OpenClaw, and OpenCode forms.
- Drop an unused field from the models.dev index.
2026-10-01 23:20:06 +08:00
Jason 882344b698 feat(pi): fill thinkingLevelMap of a fetched model
Without a thinking map, Pi shows off..high for a custom model and hides
xhigh and max, even when the upstream supports them. When a fetched
model is picked and has no map yet, fill one:

- Use the reviewed map of the Pi preset at the same endpoint, but only
  under the same API and when the compat it relies on does not conflict
  with what the user wrote. Missing compat keys are added to the model,
  and for openai-completions the thinking format and reasoning_effort
  support that Pi detects must match the preset's.
- Otherwise, when Pi sends levels verbatim as reasoning_effort
  (openai-responses, or openai-completions detected as the OpenAI
  thinking format), derive the map from models.dev effort levels using
  Pi's own getEffortThinkingLevelMap rule.

A model with its own baseUrl is now looked up by that address. Update
the Pi design docs accordingly.
2026-10-01 23:20:06 +08:00
Jason f11ac73366 feat(pi): fill capabilities of a fetched model from presets and models.dev
Apply the same lookup to Pi custom providers: picking a fetched model
fills contextWindow, maxTokens, reasoning, and image input when they are
still empty. thinkingLevelMap is left untouched.

Update the Pi design docs, which previously forbade inferring model
capabilities. That rule came from an earlier implementation that voted
across providers by model name and produced wrong values; the docs now
keep the lesson as constraints (match the provider by endpoint first,
fill only empty fields, never generate a thinking map).
2026-10-01 23:20:06 +08:00
Jason 8fbcea9ff0 feat(providers): fill model metadata when picking a fetched model
A provider's /models endpoint only returns model IDs, so a model picked
from the fetched list left its context window, output limit, reasoning
levels, and modalities empty, while the same model in a preset had them.

When a model is picked in the Codex, OpenClaw, Hermes, OpenCode, or
MiniMax Code form, look up its metadata field by field:

1. the preset model with the same endpoint (hand-verified values);
2. the models.dev provider whose API endpoint matches;
3. the vendor's own models.dev entry, without its price.

Endpoints match only when host and path are equal after dropping a
trailing version segment or request path, so plans that share a host
(z.ai standard vs Coding Plan, OpenCode Zen vs Go) never borrow each
other's values. Models are never matched by name across providers: the
same model ID often has very different context windows per deployment.

Only empty fields are filled, and reasoning or image input is only ever
turned on. models.dev is prefetched once the model list is fetched and
shared with the usage pricing import; when it arrives late the row is
filled again, located by its stable row key. A single toast asks the
user to review what was filled.
2026-10-01 23:20:06 +08:00
Jason fff09fab85 refactor(usage): extract shared models.dev fetch module
Move the models.dev download, response types, and model ID normalization
out of the pricing module into src/lib/modelsDev.ts so other features can
reuse the same data. The query key becomes ["models-dev"] because the
cache now holds the raw catalog rather than pricing-only data.
2026-10-01 23:20:06 +08:00
Jason 7c0d0fc615 chore(sponsors): update Kimi Open Platform invite track IDs
Switch the Kimi Open Platform invite links to the new campaign trackers:
mainland platform.kimi.com links now use track-7cf2b91dcde043eda6ef9a95951a042c
and global platform.kimi.ai links use track-674ed6e2af924a5682a87421f7cf753a.

In the four READMEs this replaces the previous track IDs in the sponsor banner
and API call to action. The Kimi and Kimi Global presets for Claude Code,
Claude Desktop, Codex, OpenCode, OpenClaw, Hermes and Pi now carry the same
tracked links in websiteUrl, so sign-ups from inside the app are attributed as
well. The aff=cc-switch tag is kept everywhere; the console API-key deep links
and the Kimi Code subscription links are unchanged.
2026-09-30 20:04:20 +08:00
Jason c26e6ec3c1 refactor(proxy): route retry-loop bookkeeping through outcome helpers
Success, provider failure, neutral failure and terminal failure each
had copies of the breaker/status/in-use/failover-switch updates inside
forward_with_retry_inner. Move them into note_attempt, finish_success,
record_provider_failure, finish_neutral_failure and
record_failed_request so the loop no longer writes routing state
directly.

Add characterization tests that drive forward_with_retry against a
scripted upstream and assert ProxyStatus, current_providers and
circuit breaker counts on every exit path. No behavior change.
2026-09-30 20:04:20 +08:00
Jason 8f4a87d0cd feat(mcode): let users choose the provider key instead of a random UUID
MiniMax Code shows custom_provider:<key>/<model> in its model picker, so the
UUID CC Switch used as the key leaked into the UI. New MCode providers now
take a user-entered key, prefilled from the preset, like OpenCode. Existing
providers keep their UUID and the key is locked when editing, since MCode's
default-model references embed it.

- Duplicating an MCode provider generates an unused <key>-copy key.
- Adding rejects a key already used by a database row or any live
  custom_provider node, including MCode's own account providers. The check
  and save run under the MCode switch lock, and the live write re-checks the
  key while holding config.yaml.lock so it never replaces a node MCode
  created in the meantime.
2026-09-28 13:00:14 +08:00
Jason 6064fc1c5c refactor(health-check): drop connectivity check settings and dead batch/log paths
The reachability probe treats any HTTP response as reachable, so its timeout, retry and slow-threshold knobs barely change the outcome. Remove the Advanced settings panel and its get/save commands; the probe now always uses the built-in defaults (8s timeout, 1 retry, 6000ms slow threshold).

Also remove dead code: the batch check command had no frontend caller, and stream_check_logs was written on every check but never read. Stop writing it and drop the always-empty model_used/error_category result fields. The table, its migrations and the periodic cleanup stay so existing rows age out.
2026-09-27 22:38:42 +08:00
Jason 69ff69dd7f refactor: share editor, write and lookup helpers across apps and drop dead common-config code
- Resolve three-way editor conflicts in one place (ConflictPolicy::resolve)
  and share JsonChange between the Claude and Gemini editors
- Move the duplicated Codex/Grok editor helpers into editor_toml and chain
  TOML steps through TomlSteps instead of per-app wrapper patches
- Start every engine write through AppWrite::begin so the lock and the
  pre-write recovery check cannot be skipped (the Codex edits-only path
  missed it)
- Look up the direct provider, the in-use provider and the proxy origin
  through single helpers; read the mode file once per status poll, settle
  mode and written records in one state update, and query one row for the
  provider existence check
- Drop the common-config merge chain in live.rs that no longer changed
  anything, the unused config API and snippet helpers on the frontend
- Share the live-edit conflict retry flow, the inactive-fields panel and
  the editor-view app list across dialogs and editors
2026-09-27 22:38:42 +08:00
Jason b0875f4cfd fix(live): settle before acting, mark publishing before the first rename and keep live-owned fields out of new rows
- A multi-file operation records that it has started publishing in
  live-state.json before the first file is replaced; if that cannot be
  recorded nothing is published. Recovery rolls such an operation forward
  even when a published file was changed again afterwards, so an
  unpublished Codex login stash is no longer discarded.
- controller::lock_settled fails when the previous operation cannot be
  finished, and entry points refuse to save or switch instead of writing
  rows the leftover operation cannot restore. Startup, detach and exit
  log the failure per app and continue.
- Saving, adding and syncing the current provider take the proxy switch
  lock before reading mode and pointer, so entering proxy mode can no
  longer be overwritten by direct key fields. A new proxy address is
  written to every attached app even if one of them fails.
- Codex: the row-level model catalog pointer leaves with its provider; a
  profile selecting the built-in openai provider counts as the official
  route; exclusive fields the editor shows only because live has them are
  compared by value, so an untouched field changed by the client stays
  with the client. The add dialog sends the preset draft it projected, and
  the backend uses it as the field origin like an existing row.
- Claude editor keeps a legacy Bedrock apiKey at the top level where older
  versions read it; the Gemini editor recognizes a legacy Google card the
  same way switching does.
- The request path passes the provider it already resolved to the router
  instead of reading live-state.json twice per request.
2026-09-27 22:38:42 +08:00
Jason 08a80b90f2 fix(editor): keep live-owned exclusive fields and route tables out of provider rows
- Exclusive fields shown in the editor only because live already had them
  are no longer adopted into the row when left untouched; deleting one now
  removes it from live instead of being a no-op. Modified values still
  belong to the row. The Codex add dialog compares against a projection of
  live with an empty card, since its base already carries the preset.
- Codex editor diffs skip the route tables selected by both the base and the
  edited document, so a table renamed away from `custom` no longer leaks
  into config.toml as a global setting.
- The Claude editor takes the proxy switch lock, saves the row before
  writing live and restores the row when the write fails without leaving a
  pending operation, matching the other editors. Row rollback is shared
  through ProviderService::restore_row.
2026-09-27 22:38:42 +08:00
Jason a79d9ff1bb fix(live): keep staged bytes and roll forward interrupted writes around external edits
- commit_staged keeps the temp file when the rename fails, so a pending
  operation can still be finished later; one-shot StagedWrite::commit
  removes it itself.
- Recovery rolls forward once any file carries its planned bytes: files
  still at their old bytes are published, files changed by someone else
  are left alone, and the pointer/mode still land (RolledForwardExcept).
  Only an operation that never published anything is discarded.
- Entry points take the switch lock and settle the previous operation
  before reading mode, pointer, auth or stash (controller::lock_settled);
  each app writer refuses to continue after finishing a leftover
  operation so the caller retries with fresh state.
- Codex proxy contract includes the official login identity, so two
  official routes for different accounts swap auth.json.
- An unreadable Codex login stash is never overwritten by a write that
  would store a login into it.
2026-09-27 22:38:42 +08:00
Jason bd9e8c90c0 fix(gemini): ask for a restart after switching Gemini CLI
Gemini CLI loads ~/.gemini/.env only at startup (and when validating the
auth method), and never overwrites a variable already in its environment,
so a running session keeps the old key, endpoint and model after a
switch. The switch toast now asks for a restart, as it already does for
Codex and Grok Build.

The user manual said the opposite in three places (the quick start
table, the switch page and the FAQ), claiming Gemini CLI re-reads .env on
every request; all three languages now say a restart is needed. The
README and the routing page now list Gemini CLI among the apps that show
a restart notice.
2026-09-27 22:38:42 +08:00
Jason aa99584851 docs: translate key-field switching docs to English, Japanese and German
Brings the README FAQ (English, Japanese, German) and the English and
Japanese user manuals in line with the Chinese master text.

- README FAQ: switching no longer touches plugins, hooks and other shared
  settings; a model changed in the client resets on switching back;
  routing keeps the direct provider and writes it back on exit; the
  device-local files; the restart note now covers Gemini CLI.
- Manual: the config editor section replaces backfill; the add dialog
  follows the same rules; quick toggles are marked global or per
  provider; the keys each app's switch changes; config ownership,
  malformed files and the first-write backup; routing writes back the
  direct provider; the Codex login is restored when switching back to an
  official card; Gemini auth follows the provider type.
2026-09-27 22:38:42 +08:00
Jason 59b851969e docs(zh): fix leftover restart and routing backup wording
- The routing feature bullet and the quick start still said only Codex
  and Grok Build may need a restart when a switch changes the model;
  Gemini CLI does too, as the FAQ already says.
- The routing page still said routing backs up the original config and
  restores it; disabling routing now writes back the direct provider,
  and the only backup is the one taken before the first write.
2026-09-27 22:38:42 +08:00
Jason db468c39f0 docs(zh): describe key-field switching in the README FAQ and user manual
Chinese master text for the new switching behavior; translations follow
once it is final.

- README FAQ: switching no longer touches plugins, hooks and other shared
  settings (replaces the common-config snippet answer); a model changed
  in the client resets on switching back; routing keeps the direct
  provider and writes it back on exit; the device-local files
  (live-state.json, the Codex login stash, first-write backups) and the
  restart note now covering Gemini CLI.
- Manual: the editor section (what it shows, global vs per-provider
  fields, edit conflicts, inactive fields) replaces backfill; the add
  dialog follows the same rules; quick toggles are marked global or
  per-provider; the keys each app's switch changes; the config ownership
  and malformed-file sections; routing writes back the direct provider;
  the Codex login is restored when switching back to an official card;
  Gemini auth follows the provider type.
2026-09-27 22:38:42 +08:00
Jason 143bc4613b feat(providers): add Codex, Gemini CLI and Grok Build providers through the key-field editor
The add dialog for these apps now follows the same rules as the editor
and Claude Code's add dialog. Picking a preset (or the custom template)
projects it onto the current client files, so the editor shows what the
files will look like after switching to the new provider. On save, key
fields and provider-exclusive fields go to the row and other edits go to
live as global settings with a three-way check. Before, anything typed
there besides key fields was stored in the row and never took effect.

- Adding and editing share one backend path per app. When the new
  provider is the first one, its key fields and the direct pointer are
  committed in the same write; the Gemini and Grok editor writers take a
  set_pointer flag for this, as Codex's already did.
- The form reports each projection to the dialog as the three-way base.
  Only the latest request counts; while a projection is pending or after
  it fails, the add saves the row only. Every reset of the shown content
  re-projects or clears the base, otherwise global settings missing from
  the editor would be written to live as deletions. Grok's official card
  has no editor and keeps its seed flow.
- Remove six unused common-config strings and rewrite the empty-state
  hint that still described the snippet; it now explains key-field
  switching and also shows for Grok Build.
2026-09-27 22:38:42 +08:00
Jason fb56453702 feat(gemini,grok): switch Gemini CLI and Grok Build by replacing only key fields
Switching, adding, editing, sync and entering or leaving local routing now
write Gemini CLI and Grok Build through the write engine. Whole-file
rewrites, backfill before switching and snippet merging are gone.

Gemini CLI:
- Only the key-field lines in .env change; comments, order and the user's
  own variables stay. In settings.json only security.auth.selectedType and
  model.name change. Both files commit as one operation.
- selectedType follows the provider type (Google login for official cards,
  API key otherwise) instead of the row, so a /auth choice an older version
  stored in a row can no longer send a third-party card through Google
  login.
- Local routing writes the route's model name along with the local address
  and placeholder key, so switching to a route with another model rewrites
  the client, as Claude Code does.
- Vertex rows pass validation with GOOGLE_API_KEY or
  GOOGLE_GENAI_USE_VERTEXAI instead of GEMINI_API_KEY.
- The common-config snippet is frozen like Claude's and Codex's; its UI and
  the explainer dialog are removed. New rows keep commonConfigEnabled: true
  for older versions that sync them.

Grok Build:
- Only models.default and the [model."<name>"] table CC Switch wrote
  change; MCP servers and the user's other tables stay.
- The written table name is recorded in live-state.json in the same
  operation, and switching away removes the recorded table rather than
  whatever models.default points to now, so a default changed in Grok's
  /settings no longer leaves the old provider's table behind. Without a
  record (just upgraded), the table is inferred from the current row.
  Tables holding the proxy placeholder key are always removed.
- Local routing writes the route's table pointing at the local
  /grokbuild/v1 endpoint with the placeholder key through the engine. An
  official card cannot be a route.

Editors:
- Both editors show what the client files will look like after switching.
  On save, key fields go to the row and other edits go to live as global
  settings with a three-way check. Gemini row fields that never reach live
  can be added to the global settings; Grok's can be copied, as in Codex.
  The TOML diffing shared with Codex moves to editor_toml.rs.

Also:
- First-run import no longer moves key fields (GEMINI_MODEL, Codex
  model_reasoning_effort and the like) from the default row into the
  snippet, where a frozen snippet would lose them.
- The .env patch can remove keys unconditionally and parse entries.
2026-09-27 22:38:42 +08:00
Jason 0e6430abdb feat(codex): switch Codex by replacing only key fields
Switching, adding, editing, sync, universal providers and entering or
leaving local routing now write Codex through the write engine. Only
routing, credential, model, reasoning, context-window and compatibility
fields are replaced; MCP servers, [projects], comments and the user's own
provider tables stay byte-for-byte. auth.json, config.toml, the model
catalog, the managed-account marker and a device-local login stash commit
as one operation, so a crash at any point rolls forward on startup.

- Third-party routes are always written as [model_providers.custom] with
  the key in experimental_bearer_token. Rows that use another id, a
  top-level openai_base_url or a legacy reserved table are normalized on
  the way out. Switching to an official card leaves a dormant custom table
  (local proxy address plus placeholder credential) so older third-party
  sessions can still resume.
- requires_openai_auth follows the route's auth and whether a login is on
  disk; keyring and ephemeral credential stores are handled separately.
- A switch is refused before anything is written when the active profile
  overrides the route, the selected table is missing or built in, a
  keyless row would fall back to the official login, or the row has no
  auth.
- Provider tables an older version wrote are removed only when they can
  be proven to be CC Switch's. Tables a profile references are kept, and
  reserved ids that cannot be proven are renamed.
- Provider-exclusive fields, including web_search and a row's own
  model_catalog_json, are removed on the way out only when live still
  holds the previous provider's value.
- No more backfill. A native ChatGPT login that CC Switch deletes or
  overwrites is kept in ~/.cc-switch/codex-login-stash.json (0600, not
  synced) and restored when switching back to an official card; logging
  out on an official card discards it. Login snapshots already in
  official rows seed the stash once. A leftover API key in auth.json is
  deleted only when it matches a third-party row's key.
- Managed accounts fetch their token before taking the lock. If the CLI
  refreshes the login during a switch, the switch stops before writing
  anything. Once files are published, a later failure no longer rolls the
  row back; startup finishes the switch.
- Local routing: third-party routes get a fixed local-proxy table and the
  official route uses cc-switch-official plus the client's own login. The
  contract covers every field written, so a route switch with the same
  contract touches no files.
- The Codex editor shows what config.toml will look like after switching.
  On save, key fields go to the row and other edits go to live as global
  settings with a three-way check. Row fields that never reach live are
  listed read-only and can be copied.
- The Codex common-config snippet is frozen like Claude's; its UI and the
  TOML snippet command are removed. New Codex rows keep
  commonConfigEnabled: true for older versions that sync them.
- The write engine can delete a file as part of an operation.
- Integration-test mutexes tolerate poisoning so one failure no longer
  cascades into unrelated tests.
2026-09-27 22:38:42 +08:00
Jason 15c0b3ce23 feat(proxy): keep a per-app direct/proxy mode instead of backing up live files
Local routing no longer takes over a client by backing up its live config
and restoring it later. Each routable app (Claude Code, Codex, Gemini CLI,
Grok Build) now has a device-local mode in live-state.json: direct or
proxy, whether it is attached, the proxy route, and the contract written
into the client. proxy_config.enabled is kept only as a mirror for older
versions.

- Entering proxy mode writes the route's contract (local proxy address,
  placeholder credential, model aliases, provider-exclusive fields) into
  the client; leaving re-projects the direct provider. No backups.
- The proxy route is independent of the direct provider. Switching while
  routing changes only the route; if the contract is unchanged the client
  file is neither read nor written. Failover records the new route and
  never touches client files.
- Current-provider reads go through mode::current::provider_for with an
  explicit purpose (direct pointer vs. the provider in use); a unit test
  rejects direct calls to the old getters.
- Quitting detaches (writes the direct config back, keeps the mode);
  startup re-attaches. Legacy takeover state is migrated on startup:
  backup rows are saved to ~/.cc-switch/backups/proxy-live-backup/ and
  deleted so an older build cannot replay them.
- "Stop routing" returns every app to direct and then stops the server.
  Applying a project no longer disables routing; it moves the direct
  provider or the route depending on the mode. A provider used as the
  route cannot be deleted.
- Codex and Grok Build still use their whole-file writers for proxy
  writes, and all three non-Claude apps for leaving; the direct row is
  backfilled from live before entering (never for the official Codex
  card), and leaving to an unbound official Codex card writes config.toml
  only.
- The provider list marks the direct provider while routing.
- Removes the backup/restore/hot-switch/crash-recovery paths from the
  proxy service, the unused tray Auto item, and the downgrade golden test.
2026-09-27 22:38:25 +08:00
Jason 63ed5002e5 feat(claude): switch Claude Code by replacing only key fields
Switching, adding the first provider, editing the current provider,
sync, universal providers and restoring after the local router all
write settings.json through the write engine now. Only endpoint,
credential, model and protocol fields plus the provider's own switches
are replaced; hooks, plugins, permissions and everything else stay
byte-for-byte. The file write and the current-provider pointer commit
as one operation, so a crash in between rolls forward on startup.

- No more backfill, no common-config snippet and no context-window
  injection for Claude. Window values left behind by the old injection
  are cleaned up from a frozen list.
- The provider editor shows what settings.json will look like after
  switching. On save, provider fields go back to the row and other
  edits go to live as global settings, with a three-way check against
  external changes and a dialog to resolve conflicts. Row fields that
  never reach live are listed with a one-click add.
- New Claude rows keep commonConfigEnabled: true so older versions that
  sync the row still merge their snippet.
- Presets drop timeout and telemetry keys; a test now requires Claude
  presets to contain only key or provider-owned fields.
2026-09-27 22:38:25 +08:00
Jason 81df5a08d1 feat: add a key-field write engine for client config files
Client config files are now changed through one engine instead of being
rebuilt from provider snapshots:

- Key-field definitions live in live/floor.rs and are shared with the
  common-config extraction.
- JSON, TOML and dotenv patchers change only the keys they name. Other
  keys, values, order, comments, indentation, line endings and BOM stay as
  they are, and writing the same target twice is byte-stable.
- A file that cannot be parsed is refused and left untouched; it is never
  rebuilt from an empty document.
- Each write is staged next to the target, re-read and compared before the
  rename, and replanned on top of the new content when another writer got
  there first. The first engine write of every file keeps a byte-for-byte
  backup under ~/.cc-switch/backups/live-first-write/.
- Multi-file operations record their intent in ~/.cc-switch/live-state.json
  (0600) before publishing. After a crash the operation is rolled forward
  from the staged files, discarded if nothing was published, or abandoned
  if the files were changed externally. Recovery runs at startup and before
  every operation.

Claude Desktop is the first client moved onto the engine. Its four files
are written in one operation; the gateway profile keeps the user's own
settings, seeds disableDeploymentModeChooser and coworkEgressAllowedHosts
only when missing, and is written owner-only. Restoring the official app
now clears only the CC Switch key fields instead of deleting the profile,
and a meta file that cannot be parsed is refused instead of replaced.
2026-09-27 22:38:24 +08:00
Jason b9c2739392 fix(database): reclaim the whole freelist on incremental vacuum
PRAGMA incremental_vacuum frees one page per result row, and the pages
are only released while the statement is being stepped. execute_batch
steps it once, so startup cleanup and periodic maintenance reclaimed a
single page each time and the database file never shrank after pruning.

Add Database::incremental_vacuum_on_conn, which drains every result
row, and use it in both places.
2026-09-27 22:22:20 +08:00
Jason 46fa2e0e49 refactor(usage): drop per-provider and global cost multipliers
Relays already price in "official price x their own multiplier", and the
real spend is visible from the relay's own balance or usage script. A
second copy of that multiplier in CC Switch only drifts out of sync, and
it only ever applied to requests going through local routing (session
log imports always recorded 1.0), so users setting it in direct mode saw
no effect at all.

- Remove the pricing section from the provider editor (it was also shown
  for Pi and MiniMax Code, which never go through local routing).
- Remove the default multiplier column from the usage pricing defaults;
  keep the global pricing model source (request vs response model) and
  say it only applies to routed requests.
- Remove the get/set_default_cost_multiplier commands and DAO methods.
- Routed requests are now always recorded at multiplier 1. Existing log
  rows keep the multiplier they were recorded with.
- Keep costMultiplier / pricingModelSource in ProviderMeta, unread, so
  older devices syncing the same rows do not lose their values; the
  proxy_config column stays as well, so no schema migration is needed.
2026-09-27 22:21:45 +08:00
Jason bcb83d7ccd test: add golden tests that lock live-config behavior before the switching refactor
The upcoming refactor replaces whole-file live writes with key-field
replacement and splits direct and proxy modes. These tests pin what must
survive it, and pass on the current code.

Backend (`cargo test --test golden`, snapshots under
src-tauri/tests/golden/snapshots, refresh with CC_SWITCH_UPDATE_GOLDEN=1):
- mcp_bytes: MCP projection bytes in ~/.claude.json, Codex config.toml
  and Gemini settings.json, and that switching providers leaves the MCP
  section untouched.
- import_rows: raw DB rows written by deep-link import and first-run
  import (including seeded official rows and the generated snippet).
- file_modes (Unix): which client files a switch writes and their
  permission bits; existing 0600 files are never loosened.
- claude_key_fields: after a switch, live key fields are exactly the
  target provider's, across third-party, Bedrock, Vertex, apiKeyHelper
  and official providers, with or without common config.
- codex_red_lines: 25 Codex invariants (index at the top of the file),
  asserted as outcomes: the config loads on Codex 0.149, the official
  login never reaches a third-party URL, refused switches leave files,
  current provider and rows untouched.
- downgrade_contract: the previous release detects takeover by the
  PROXY_MANAGED literal, rebuilds live from current rows without backup
  rows, and replays leftover backup rows.

Frontend: ProviderForm submit payloads for ten representative Claude and
Codex presets.

Three spots are sorted before comparing because their output order is
not deterministic today (MCP maps written from a HashMap, Gemini .env
parsed into a HashMap). One ignored test records a known gap: exiting
the proxy restores a login the user logged out of while on a
third-party Codex route.
2026-09-26 23:05:45 +08:00
Jason 2185498e88 fix: stop shared config and live files from leaking provider settings
Common config snippet (Claude Code):
- Treat custom request headers (`*_CUSTOM_HEADERS`, `HEADERS`) and any
  key containing COOKIE or AUTHORIZATION as credentials. Header names in
  general stay shareable (CLAUDE_CODE_ATTRIBUTION_HEADER,
  OTEL_EXPORTER_OTLP_HEADERS).
- Never share key fields: all ANTHROPIC_* settings, the protocol
  selectors (CLAUDE_CODE_USE_BEDROCK/VERTEX/...), AWS_* and Vertex
  regions, and top-level model/apiKeyHelper/fallbackModel. Switching
  from Bedrock to the official provider no longer keeps Claude Code on
  Bedrock, and a /model choice no longer follows every switch.
- Entries an existing snippet already shared are stripped from the
  current provider's row on its next switch-away, so they cannot get
  stuck in the wrong provider. A provider carrying AWS or Google
  credentials keeps that family's selectors and regions.

Live files holding keys are now written owner-only (0600): Codex
auth.json and config.toml, Claude Code settings.json and Grok Build
config.toml. New files used to follow the umask (0644).

Presets:
- E-FlowCode and PIPELLM no longer ship personal preferences; Codex
  E-FlowCode drops `personality`.
- AWS Bedrock (API Key) stores the key in env.AWS_BEARER_TOKEN_BEDROCK,
  which Claude Code reads. The key field and the proxy read it too;
  existing rows with a top-level apiKey keep working and are edited in
  place.
- A preset scan keeps preference keys out of Claude and Codex presets.

Remove dead code only reached from tests: ProxyService::start_with_takeover
and its helpers, and the ConfigService sync_current_providers_to_live
family.

Behavior changes: ANTHROPIC_* settings such as ANTHROPIC_LOG are no
longer shared; a Bedrock or Vertex provider whose selectors only lived in
the shared snippet may need them re-entered once.
2026-09-26 23:03:42 +08:00
Jason f33f9c41cb fix(grokbuild): keep provider model table selected when backfilling live config
Grok Build's /settings "Default model" rewrites models.default in
~/.grok/config.toml. Selecting a built-in model while a third-party
provider was active left the provider's own [model.*] table
unreferenced; the switch backfill stored that config verbatim, and
switching back then failed validation with "missing [model.\"...\"]".

When models.default is missing or dangling during backfill and the
provider's original default table is still present, restore it.
2026-09-26 21:51:48 +08:00
Jason 1ee2fdc3a7 docs(user-manual): refresh the zh/en/ja manual for v3.20.4
- Cover Grok Build, Pi and MiniMax Code across install, interface,
  settings, presets, MCP, Skills, prompts, sessions, local routing,
  config files and env conflict pages
- Rename proxy/takeover wording to local routing and update settings
  paths to the Routing / Auth / Usage Statistics tabs
- Rewrite 4.5 as the connectivity check that replaced model test in
  v3.16.3 (no real model request, no test model to configure)
- Fix stale facts: official subscription quota is opt-in via the
  Official Subscription template since v3.16.2, Codex keys live only in
  config.toml experimental_bearer_token, deep link content/config must
  be Base64, SQL export includes usage logs, onboarding skip writes
  hasCompletedOnboarding in ~/.claude.json
- Backfill the website-only Codex preset section (native Responses vs
  Chat Completions) and align UI labels with the v3.20.4 locales
- Retranslate en/ja from the approved zh master, keeping all three
  languages line-aligned with matching anchors
2026-09-26 15:36:19 +08:00
Jason e0f70019b2 docs(guides): refresh Codex guides for native Responses presets and the renamed auth toggle
- Backfill website-only edits: Kimi direct-connection section, desktop FAQ cross-links, stray </content>
- Retitle the DeepSeek guide around Chat-format providers; v4-pro, Kimi, GLM and MiniMax now connect directly
- Rename the toggle to "Keep official login for direct switches"; document auth.json removal and takeover preservation
- Prefill gpt-5.6-sol, mention the Kimi Global presets, and use routing takeover terminology
- Remove the outdated, unreferenced proxy-guide-zh.md
2026-09-26 12:06:52 +08:00
Jason 0555c09d9a docs(readme): use support@ccswitch.io as the sponsorship contact 2026-09-26 10:37:04 +08:00
Jason 854c9f5fbf docs(readme): restructure all four READMEs and move dev docs to CONTRIBUTING
- Reorder sections (download and quick start before features), add a per-tool
  capability table, and document local routing, projects, the OAuth auth
  center (with a ToS risk note), quota display, usage without routing,
  S3 sync, and the CLI tool manager
- Rewrite the data-storage FAQ; add FAQs for routing addresses, OpenAI/Gemini
  upstreams, connectivity checks, WSL, and the community CLI
- Fix first-launch import (Claude Desktop is not imported), Pi's Enable
  button, and the non-existent Markdown preview; unify on "local routing"
- Move architecture/dev guide/project structure into CONTRIBUTING.md and
  refresh it (Node/pnpm/Rust versions, CI-aligned checks, test isolation,
  four locales, no ESLint/pnpm lint)
2026-09-25 22:54:32 +08:00
Jason 913a3bf23e docs(readme): refresh stale facts across all four READMEs and add a tagline and quick nav
- Add Pi (10 managed apps), scope tray/MCP/Skills per app, document additive apps
- Fix "signature bypass" wording (Hide AI Attribution), common-config FAQ labels,
  official-login/restart FAQs, and first-launch auto-import note
- Correct Linux requirements (glibc 2.35 + WebKitGTK 4.1) and release asset names
- Update dev env (Node 20.19+/22.12+, pnpm 10, Rust 1.95), drop broken coverage cmd
- Point release notes to v3.20.4 and SSSAiCode to sssaicodeapi.com
- JA: replace the stale unsigned-macOS instructions; fix Chinese FAQ anchors in
  SUPPORT.md and issue templates
2026-09-25 21:30:49 +08:00
Jason 8ab8eebdd0 fix(updater): upgrade Codex standalone installs in place instead of via npm
Updating a CLI installed outside npm fell back to a bare `npm i -g`, which
silently installed a second, npm-managed copy instead of upgrading the one
the command line actually runs.

- Codex standalone installer (install.sh / install.ps1): rerun the official
  installer with CODEX_INSTALL_DIR / CODEX_HOME pinned to the detected
  install. `codex update` is not used: it pipes `curl | sh` without
  pipefail and reports success even when the download fails.
- Keep the npm self-heal for broken Codex installs away from standalone
  installs whose entry lives in a Homebrew/nvm bin directory.
- When the default install is a native binary outside node_modules and its
  channel is unknown (winget, Scoop, Nix, manual download), stop falling
  back to npm: the probe reports it as unmanaged, the About page skips it
  with a localized hint, and the backend refuses to run it.
- Add a Claude Code cask regression test on top of the Homebrew cask
  support from #6572.

Fixes #7650
2026-09-25 20:29:49 +08:00
Jason bc4cce047b feat(mcode): pass Pi model compat through to MiniMax Code presets
MiniMax Code 0.5.x reads a whitelist of `models.<id>.compat` keys from
config.yaml and layers them over the compat it synthesizes, so Pi's
per-model compat can be reused instead of dropping every preset that
carries one.

- Keep a model when every compat key is one MiniMax Code reads, and write
  the compat verbatim. kimi-k3 stays out: `deferredToolsMode` is unknown
  to MiniMax Code's embedded pi-ai.
- Map adaptive-thinking Claude models to `thinking.effortOptions`, since
  MiniMax Code ignores Pi's thinkingLevelMap and compat-blind versions
  would otherwise fall back to budget_tokens.
- Presets grow from 40 to 77; only AWS Bedrock remains excluded.
2026-09-25 20:27:29 +08:00
Jason 9154f41846 fix(mcode): replace misleading empty-state copy that points to a missing import button
MiniMax Code intentionally has no "Import Current Config" button (same as
Pi): custom providers in config.yaml are merged into the list on every
load, and the official MiniMax account provider plus its sign-in state
are owned by MiniMax Code, which CC Switch refuses to write. The empty
state still showed the generic copy asking users to click that button.

Give mcode its own empty-state title and description in all four
locales, route Pi and mcode through their own namespaces in
ProviderEmptyState, and add a regression test.
2026-09-25 20:27:29 +08:00
Jason 65d0d741f6 feat(about): add MiniMax Code install/update detection
Add MiniMax Code (mcode) to the About page tool list: version probing,
latest version from the npm registry (@minimax-ai/code), and silent
install/update actions.

- Install runs the official installer (install.sh / install.ps1) with an
  npm fallback. npm installs pass --ignore-scripts=false,
  --include=optional and --allow-scripts for @minimax-ai/code and
  better-sqlite3, since npm 12 blocks dependency install scripts by
  default and SQLite breaks without them.
- `mcode update` is deliberately not used: without a TTY it only prints
  the command and exits 0 without installing, which would mask failures
  in silent lifecycle runs.
- Script installs are updated in place by re-running the official
  installer with MCODE_INSTALL_DIR set to the detected root. Custom
  install dirs are claimed via the installer's install.json receipt
  (UTF-8 BOM tolerated, as Windows PowerShell 5.1 writes one), falling
  back to the default .minimax-code directory name. The Windows layout
  keeps mcode.cmd at the install root, so only the POSIX layout strips
  a bin segment.
- npm global installs anchor to the sibling npm with the same flags.
- Search the installer launcher dirs ($MCODE_INSTALL_DIR,
  ~/.minimax-code) ahead of generic Node locations.
- Update the one-click install hint in all four locales, also adding the
  previously missing Grok Build.
2026-09-25 20:27:29 +08:00
Jason a06a41ec35 docs(sponsor): update FluxA paid API count to 13,000+ 2026-09-24 22:42:55 +08:00
Jason 3ed5892532 fix(ui): desaturate inactive brand-colored icons in app switcher
Colored brand icons (Claude, Gemini, OpenCode, OpenClaw, Hermes,
MiniMax Code) kept their full color when inactive, so only the pill
background distinguished selected vs unselected. Monochrome icons
(Codex, Grok Build, Pi) already dimmed via currentColor.

Apply grayscale + reduced opacity to inactive colored icons, restoring
full color on hover or when active.
2026-09-24 22:42:55 +08:00
Jason 43e1d99084 docs(release): add v3.20.4 release notes 2026-09-22 22:17:05 +08:00
Jason 84efe1fb5b chore(release): v3.20.4 2026-09-22 22:17:05 +08:00
Jason 42200b42a6 feat(pricing): add Grok 4.7 and GLM-5.3-FlashX
Both confirmed against the vendors' official pricing pages (docs.x.ai,
docs.z.ai) and match models.dev. Grok 4.7 records the base (<200K) tier
like 4.5/4.6.
2026-09-22 20:16:27 +08:00
Jason bccfaf37f3 fix(i18n): remove outdated OpenCode Go referral offer 2026-09-22 20:16:27 +08:00
Jason 54640b9559 fix: add SudoCode.chat alternate API endpoint 2026-09-22 20:16:27 +08:00
Jason f8821c03e3 feat(settings): add a GitHub star prompt to the About card
Downloads and updates now go through ccswitch.io, so users no longer
pass through the GitHub repo and rarely star it. Surface the ask where
they still look: between the app title and the action buttons.

- Show a one-line prompt in the primary color that opens the repo,
  followed by a pointer toward the GitHub button
- Move the GitHub button to the front of the row so the pointer lands
  on it, tint it with the primary color (kept below the solid "check
  for updates" action), and add a slowly spinning star that stops
  under prefers-reduced-motion
- Add settings.starPrompt to all four locales
2026-09-22 20:16:27 +08:00
Jason bbee784dd1 feat(presets): add Soshow aggregator presets
Add non-sponsor presets for Soshow Model Market (maas.so-show.com) across
Claude Code, Claude Desktop, Codex, OpenCode, OpenClaw, Hermes and Pi, plus
its icon.

- Claude Code sets CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS, which the vendor's
  Model API doc requires for the Claude protocol, and points the Haiku tier at
  claude-sonnet-5 since the market offers no Haiku model.
- Claude Desktop declares only the sonnet/opus routes: direct mode cannot map
  models, so passthroughRoutes() would expose a Haiku route that 404s.
- Codex uses native Responses at /v1. The route is not in the vendor docs but
  exists (unknown paths return 404, /v1/responses returns 401 without a key).
- OpenCode, OpenClaw, Hermes and Pi use the Anthropic protocol with the two
  Claude models, matching the vendor's OpenClaw guide.
- Gemini CLI is skipped: the native generateContent routes return 404.
2026-09-22 20:16:27 +08:00
Jason 4b1ec8b55b fix(ci): stabilize frontend tests and verify renderer build 2026-09-22 20:16:27 +08:00
Jason 970a6e4fbc docs(partner): add a FluxA purchase link and tighten its sponsor blurb
The FluxA sponsor row only linked from the banner image. Append the same
"via this link" call to action the other sponsor rows use, pointing at
the TokenPlan marketplace page, and collapse the blank line between the
two paragraphs into a single line break to save vertical space.
2026-09-21 22:28:06 +08:00
Jason f6c9982240 fix(claude-desktop): expose the 1M context variant for DeepSeek V4 routes
The DeepSeek V4 family has a 1M context window - every Codex catalog in
this repo records 1048576 for these ids - but the Claude Desktop presets
that pin them left supports1m off, so the 1M variant never appeared in
Claude Desktop's model list. The [1m] suffix is a Claude Desktop local
marker: it is translated to the supports1m field at the import boundary
and stripped before upstream matching, so turning it on does not change
what is sent to the provider.

Affects Baidu Qianfan Token Plan, DeepSeek and OpenCode Go. PPIO already
had it on.
2026-09-21 21:45:23 +08:00
Jason 4d2c6f0777 feat(presets): add FluxA Token Plan partner presets
FluxA AgentMarket resells Baidu AI Cloud's international Token Plan at
partner pricing. The key it issues is used directly against Baidu's
overseas endpoint, which is a separate deployment from the existing
domestic personal plan on qianfan.baidubce.com.

- Claude and Claude Desktop use the Anthropic-compatible base
  api.baiduqianfan.ai/anthropic/tokenplan/team; Codex, OpenCode,
  OpenClaw, Hermes and Pi use the OpenAI-compatible
  api.baiduqianfan.ai/v2/tokenplan/team.
- The model lineup follows FluxA's product page table, excluding the two
  entries it marks "Coming soon". kimi-k2.6 is added on top because the
  sponsor copy names Kimi; its id and 262144 context window come from
  FluxA's own baidu-ai-cloud catalog and the domestic Token Plan preset.
- glm-5.2 is pinned to the 500k window FluxA publishes for this
  international deployment, not the 1M the domestic platform reports.
- DeepSeek V4 rows are declared text-only, matching the other hosted
  Token Plan presets, and the preset joins the existing regression guard
  that keeps those rows from failing open to image input.
- Registers the fluxa icon, the partner promotion string in all four
  locales, and the sponsor row in all four READMEs.
2026-09-21 21:45:23 +08:00
Jason 0859fa6a98 fix(presets): correct AICodeWith Codex-family endpoint to /v1
The AICodeWith site owner confirmed the Responses endpoint is
https://api.aicodewith.ai/v1. The /chatgpt/v1 path published in their
blog posts is a documentation error.

Update the Codex preset (base_url and endpointCandidates) and the
Hermes, OpenClaw and Pi presets, which reuse the same Responses
endpoint.

Existing providers created from the old preset keep the stored URL and
need to be edited manually.
2026-09-21 21:45:23 +08:00
Jason 09498c30cd fix(ui): render monochrome preset icons in the foreground color
Icons that paint with currentColor inherited the preset button's
text-muted-foreground, so the same icon looked mid-gray in the preset
selector but near-black in the form's icon preview and on the provider
card. The mismatch hit all 28 monochrome icons and was most visible on
9527CODE, whose 0.82-opacity base layer lightened the gray further.

Unselected preset icons now take text-foreground; selected ones still
inherit the button's text-white. The universal preset row gets the same
treatment. Brand-colored icons are unaffected because ProviderIcon sets
their color inline.
2026-09-21 21:45:23 +08:00
Jason 060099d72a docs(user-manual): point Chat-only Codex examples at plan endpoints
Baidu Qianfan and StepFun now ship native Responses presets for their
pay-as-you-go APIs, so the Chat-only examples name the offerings that
still route through Chat: Baidu Qianfan Coding Plan and StepFun Step
Plan. Synced across the zh/en/ja manuals.
2026-09-21 21:45:23 +08:00
Jason d8e98be2c7 fix(pricing): restore DeepSeek V4 Pro price and seed Qwen3.8 and Hy4 models
- deepseek-v4-pro returns to its peak list price 1.32/3.96/0.044. The
  2026-09-11 move to the V4.1 Flash price anticipated a cutover that
  DeepSeek retracted on 2026-09-12. Replace that repair entry with one
  that restores rows still at 0.3/1.2/0.006; keeping both would rewrite
  the row back and forth on every start.
- Seed hy4-preview (CNY 6/18/0.3 at 7.14), qwen3.8-2.4t-a95b and
  qwen3.8-27b (Alibaba Cloud international list prices).
2026-09-21 21:45:23 +08:00
Jason e06ff90f8f fix(presets): move MiniMax CN and BaiLing to their current official endpoints
- MiniMax CN: the remaining apps switch from api.minimaxi.com to
  api.minimax.cn (Anthropic root /anthropic). Usage detection recognizes
  both hosts and now matches on host labels (frontend, and the global
  host in the backend), so look-alike hosts and URLs that only carry the
  domain in their path are ignored. The quota query keeps
  api.minimaxi.com, the only CN host documented for that endpoint.
- BaiLing: the remaining apps switch from api.tbox.cn to
  api.ant-ling.com (/v1 and /anthropic) and default to Ling-2.6-1T,
  since Ling-2.5-1T is no longer listed in the current docs. Ling-2.6-1T
  is registered as text-only (vision ships as separate -VL models).
- Reword the DeepSeek V4 Pro note in the text-only registry: the vendor
  keeps serving V4 Pro after September 14, 2026.
2026-09-21 21:45:23 +08:00
Jason 48e572cc87 feat(codex): refresh CN presets from the Responses API audit
- Kimi For Coding: kimi-for-coding now serves K2.8 Preview (up to 1M,
  text+image, low/high/max); the highspeed variant stays at 256K.
- DeepSeek: default to deepseek-flash (V4.1 Flash, text+image) and
  declare deepseek-v4-pro text-only, matching the bundled official
  catalog.
- Add native Responses presets for StepFun API (CN/intl,
  step-3.7-flash), Baidu Qianfan pay-as-you-go (/v2) and iFlytek Astron
  Coding Plan; subscription plans on the same hosts stay on Chat.
- Add hy4-preview to Tencent Hunyuan, and the open-weight
  qwen3.8-2.4t-a95b / qwen3.8-27b to both pay-as-you-go Qwen platforms.
- MiniMax CN moves to api.minimax.cn and BaiLing to api.ant-ling.com.
- Disable Codex web_search on StepFun (function tools only), and
  conservatively on Qianfan and Astron, whose Responses docs document no
  hosted tools. The new hosts stay out of CODEX_NATIVE_RESPONSES_HOSTS
  so Chat plans on the same hosts are never reclassified as native.
- Tencent Enterprise Token Plans stay on Chat: /plan/v3/responses is
  listed, but TokenHub marks Kimi K2.7 Code unsupported and several
  models as server-side Chat conversions, and the catalog levels were
  verified on Chat only.
2026-09-21 21:45:23 +08:00
Jason 7f39d88584 test(claude-desktop): run Linux path tests on unix hosts and cover Linux in provider sync tests
Follow-up to #7331 (Claude Desktop 3P configuration on Linux):

- Compile the Linux config-dir resolver and its three unit tests under
  any unix test build so they run on macOS as well as CI ubuntu. The
  gate is unix rather than test because the assertions use POSIX
  absolute paths, which are not absolute on Windows.
- Open the provider -> Claude Desktop profile sync integration tests to
  Linux: add the Linux profile path helper and pin XDG_CONFIG_HOME under
  the temporary home (saved on construction, restored on drop).
- Update the support-scope table in the en/zh/ja user manuals to list
  Linux and note that the Flatpak build always uses the host ~/.config.
2026-09-21 21:45:23 +08:00
Jason 2c735bd9de fix(ui): stop expanding stale cached usage tiers on ineligible provider cards
A disabled React Query observer still returns the existing cache entry for
the same key, so a provider switched to the official subscription template
(or with its usage script disabled) kept showing the multi-plan chevron and
the expanded tiers left over from a previous script result.

Gate hasMultiplePlans with the same predicate that enables the usage query,
extracted as scriptUsageActive so the two cannot drift apart again.
2026-09-21 21:45:23 +08:00
Jason 1d5d90f4ab fix(presets): update APIKEY.FUN URLs to apikey.fan 2026-09-12 20:27:33 +08:00
Jason d695a2d77f docs(release): add v3.20.3 release notes 2026-09-11 23:23:51 +08:00
Jason 1a7250162c chore(release): v3.20.3 2026-09-11 23:23:51 +08:00
Jason 746e228852 fix(pricing): move retired DeepSeek V4 family to the V4.1 Flash tier
DeepSeek retired V4 Flash and announced that V4 Pro is routed to V4.1
Flash and billed at the Flash price from 2026-09-14 12:00 (Beijing).
Executing that callback early per maintainer decision.

- Seed `deepseek-flash` (the only currently recommended id) and
  `deepseek-v4-flash-vision-exp` so the `LIKE '{id}-%'` prefix fallback
  cannot silently bill them at zero.
- Reprice `deepseek-v4-flash` / `-0731` / `-pro` to 0.3 / 1.2 / 0.006
  with chained repair guards ordered after the 2026-08-16 peak/off-peak
  rows; `deepseek-chat` / `deepseek-reasoner` stay untouched for lack of
  an authoritative source.
- Drop `deepseek-v4-pro` from the confirmed text-only registry: it now
  lands on a vision-capable model, so the image sanitizer must fail open.
  Fixtures that relied on it being text-only switch to `qwen3-coder-plus`,
  including the two media_sanitizer tests #7286 had just pointed at it.
2026-09-11 16:56:34 +08:00
Jason 6867b64de1 fix(presets): declare third-party Token Plan DeepSeek V4 rows text-only
DeepSeek's official endpoint now routes the legacy deepseek-v4-flash id to
the vision-capable V4.1 Flash, so #7286 removed it from the confirmed
text-only registry and the id fails open everywhere. Baidu Qianfan and
Tencent Token Plan still host the text-only V4 deployments (Qianfan's
Coding Plan doc states image input returns 400; Tencent's 2026-09-10 plan
rosters list V4 only), so their preset rows must carry an explicit
inputModalities: ["text"] declaration instead of relying on the registry.

Covers the flash and pro families incl. -0731/-0813/-202605/-202606 rows,
and adds a preset test that also locks the official DeepSeek preset to
stay undeclared (fail-open).

Follow-up to #7283.
2026-09-11 16:47:47 +08:00
Jason f874803f27 feat(presets): rename DouBaoSeed preset to localized Volcengine Doubao
Rename the DouBaoSeed provider preset across all seven app preset files
and localize its display name: zh/zh-TW show "火山 豆包AI", en/ja show
"Volcengine Doubao" via the new providerForm.presets.doubaoseed key.

Following the existing Qiniu/Compshare convention, the top-level name
holds the latin fallback (so the preset stays searchable in both
scripts) and the localized name lives in the locale files. OpenCode and
Pi settingsConfig.name, which is written into the client config files,
keeps the ASCII form.

Live-config identifiers are deliberately untouched: the Codex TOML
provider name, the Hermes node key doubao_seed, Pi's providerKey, the
OpenClaw model-ref prefix, partnerPromotionKey and icon.
2026-09-11 16:47:47 +08:00
Jason 45b9a952a6 fix(codex): skip unchanged incomplete rollout tails
Follow-up to #7219. The parser counted only fully consumed records toward
the persisted byte length, so a rollout that ends mid-record or in
whitespace (a crashed session that was never resumed) always looked
shorter than its file size and was fully reparsed on every sync pass.

Count the bytes of the incomplete tail as observed too. The line cursor
still stops before the incomplete record, so it is retried as soon as the
file grows, while an unchanged file is skipped on the mtime+size gate.
Tests prove the skip via total_changes() rather than a zero import count.
2026-09-11 16:47:47 +08:00
Jason 5c05362600 fix(claude): align quick model selection with form order 2026-09-11 16:47:47 +08:00
Jason 3b1292fee5 fix(tray): show bound ChatGPT account quota for managed Codex cards
Since #6537 the tray deliberately dropped the usage suffix for Codex
official cards bound to a managed ChatGPT account: the tray only had an
app-wide subscription cache fed by whatever login the Codex CLI holds
(macOS Keychain first, then auth.json), which cannot represent
account-scoped quota safely once several accounts are bound. The provider
card queried the bound account through `get_codex_oauth_quota`, but that
result never reached the tray, so users with multiple ChatGPT logins saw
`Codex · <name>` with no quota at all.

- UsageCache: add an account-keyed `codex_oauth` snapshot map
  (`put_codex_oauth` / `with_codex_oauth`), cleared by `invalidate_all`.
- `get_codex_oauth_quota`: write the `Ok` snapshot through to the cache
  under the resolved account id and schedule a tray label refresh.
  Transport errors keep the last good snapshot; auth/HTTP failures replace
  it so the tray hides an invalid quota instead of a stale one.
- Tray: introduce `TrayUsageSource` so the refresh path and the display
  path share one decision. Managed Codex cards read only their bound
  account's snapshot (never a provider-scoped or CLI snapshot as fallback),
  default to enabled when no usage toggle has been saved (matching
  ProviderCard), and the fixed official card takes the managed path too
  when it carries a binding. Rebinding a card switches to the new
  account's snapshot immediately and a late response for the previous
  account cannot overwrite the label.
- Tests: source selection across binding/toggle/legacy shapes, label
  rendering across switch, rebind, late response, disabled and expired
  states, and the native fixed card still using the CLI subscription.

Fixes #7267
2026-09-11 16:47:47 +08:00
Jason deb0e874ff feat(claude): add "Disable Artifact Tool" quick toggle to provider editor
Some third-party Anthropic-compatible gateways (e.g. DeepSeek) validate
tool JSON Schemas with a strict regex checker and reject the Unicode
property escapes (\p{Cc}...) that Claude Code's Artifact tool emits when
its str_replace db capability is rolled out, failing every request with
400 "Invalid schema for function 'Artifact'". The failure is independent
of the model selected.

Add a sixth checkbox next to the existing Claude quick toggles that sets
env.CLAUDE_CODE_DISABLE_ARTIFACT="1" on the provider, which keeps the
Artifact tool out of the tools array entirely. Unchecking removes the key
like the other toggles do. Labels added for zh/en/ja/zh-TW and the user
manual toggle table updated in zh/en/ja.
2026-09-11 16:47:47 +08:00
Jason 5e1292736e feat(codex): refresh aggregator model presets and catalogs 2026-09-11 16:47:47 +08:00
Jason 08984ba591 feat(codex): switch Kimi presets to native Responses direct-connect
Both Kimi open platform (api.moonshot.cn/v1) and Kimi For Coding
(api.kimi.com/coding/v1) now serve /v1/responses natively and their
official Codex guides require wire_api = "responses", so the two presets
move from openai_chat (proxy Responses->Chat conversion, routing takeover
required) to openai_responses (Codex connects directly). Verified against
both endpoints with real keys using the exact Codex 0.153.4 request shape
(include reasoning.encrypted_content, reasoning.summary, hosted web_search,
replayed reasoning items) and with codex exec end-to-end tool loops.

- kimi-k3 becomes the default open-platform model and first catalog row;
  kimi-k2.7-code stays as a second row (it also answers on /v1/responses,
  although the OpenAPI only lists kimi-k3)
- drop codexChatReasoning (Chat-conversion only) from both presets and
  promptCacheRouting from Kimi For Coding (Codex sends prompt_cache_key
  itself on the native path; the proxy auto-detects the host anyway)
- declare supportsParallelToolCalls on every Kimi row, mirroring the
  official Kimi Code models.json
- no reasoning default is declared: config.toml model_reasoning_effort
  wins over the catalog default, which only marks the /model picker
- move the two presets from the Chat list to the native list in the
  preset tests; invert the prompt-cache-routing assertion
- refresh the user manual (zh/en/ja) so the "Chat-only" examples name
  providers that still use Chat routing, and correct the stale
  userAgentPresets note about Kimi Code rejecting codex-cli

No backend change. Existing Kimi cards keep their stored openai_chat
format and continue to work through routing; switch a card's upstream
format to Responses (or re-add the preset) to connect directly.
2026-09-11 16:47:46 +08:00
Jason bfbbf15cb2 fix(usage): show Claude Fable weekly limit from the usage API limits array
The Claude OAuth usage API now reports model-scoped weekly limits in a
top-level `limits[]` array (`kind: "weekly_scoped"`, `scope.model.display_name`,
`percent`) instead of dedicated top-level windows. The parser only read the
legacy windows, so the Fable limit never showed up in the provider card or
the tray.

- Extract `parse_claude_quota` and parse `limits[]` into `seven_day_fable`,
  `seven_day_opus` and `seven_day_sonnet`; scoped rows override the legacy
  window of the same name, duplicates and malformed rows are skipped, and
  legacy windows / extra usage / unknown windows are preserved.
- Tray: give Fable its own label group so it is not merged into the weekly
  max value.
- Frontend: add the `seven_day_fable` label ("Fable") in all four locales.
- Tests: parser edge cases, tray summary for both quota and flattened script
  paths, and footer rendering for inline/expanded layouts and locales.
2026-09-11 16:47:46 +08:00
Jason f21e09449a chore(provider): remove Atlas Cloud sponsor status, keep presets
Atlas Cloud is no longer a sponsor. Drop the sponsor identity everywhere
while keeping the provider presets themselves intact:

- README (en/zh/ja/de): remove the Atlas Cloud row from the sponsor table
  and delete the now-unreferenced banner image
- Presets (Claude Code, Claude Desktop, Codex, OpenCode, OpenClaw, Hermes,
  Pi): strip `isPartner` / `partnerPromotionKey` from the AtlasCloud entry
  and move it from the sponsor group to the non-sponsor group (endpoints,
  models and icon unchanged)
- i18n (zh/en/ja/zh-TW): remove `providerForm.partnerPromotion.atlascloud`
2026-09-09 17:04:53 +08:00
Jason f3b18df120 docs(release): add v3.20.2 release notes 2026-09-07 21:54:01 +08:00
Jason b6254432a9 chore(release): v3.20.2 2026-09-07 21:54:01 +08:00
Jason 389dd96cb8 feat(provider): add SoleAPI sponsor presets across all nine apps
SoleAPI (soleapi.com) joins as a sponsor, placed directly above Micu in
the sponsor order (preset files and the four README sponsor tables stay
in sync).

- Presets for Claude Code, Claude Desktop, Codex, Gemini CLI, Grok
  Build, OpenCode, OpenClaw, Hermes and Pi. Endpoints follow the
  vendor's CC Switch setup page: Anthropic/Gemini-style clients use the
  bare host, OpenAI-style clients (Codex, Grok, Responses) use /v1.
- Haiku's upstream ID carries a date suffix
  (claude-haiku-4-5-20251001), so the Desktop preset uses mappedRoutes
  and the multi-model presets list the dated ID.
- Codex defaults to gpt-5.6-sol (vendor config.toml), Gemini to
  gemini-3.8-flash.
- No OpenClaw/Hermes cost fields: site prices are official rates times
  a per-channel multiplier and change with promotions.
- Referral link only in apiKeyUrl and README hrefs; request endpoints
  stay bare.
- Inline currentColor icon traced from the vendor SVG, metadata entry,
  four-locale partnerPromotion copy, and a 1920x786 banner.
2026-09-07 20:23:32 +08:00
Jason e724270dbb fix(codex): proxy image edits endpoint
Codex's built-in ImageGen switches from `/images/generations` to
`/images/edits` whenever it references existing images (explicit paths or
the last N generated images). After #7036 the generations route is proxied,
but edits still hit an empty local route and the tool fails with 404.

Add `CodexStandaloneEndpoint::ImagesEdits` and route the four local aliases
through the same standalone passthrough as generations. The full-URL sibling
derivation now uses a shared suffix table for the Images family, so a
provider configured with either `/images/generations` or `/images/edits` as
a complete URL derives the other route; opaque URLs still fail closed.

Tests cover the effective-endpoint match, full-URL rewrites in both
directions, and an end-to-end alias test that asserts auth replacement,
body preservation (data-URL images) and usage logging with the real Images
API `input_tokens_details` shape.
2026-09-07 20:23:32 +08:00
Jason 648d89cd7b fix(provider): pin models URL for JieKou and Novita Claude presets
Both serve the Anthropic-compatible API under /anthropic but expose the
OpenAI-compatible model list under /openai/v1. The candidates derived
from the base URL (/anthropic/v1/models, and /v1/models and /models at
the stripped root) all return 404, so "Fetch models" could never
succeed for either provider.

Also drop the JieKou assertion that pinned the absent modelsUrl, which
locked the broken behaviour in.
2026-09-07 20:23:32 +08:00
Jason ccc140a2c0 feat(pricing): refresh seed for September 2026 vendor price changes
Audit run of /jason-update-model against models.dev, every change confirmed
on the vendor's own pricing page.

New row:
- qwen3.8-flash: 0.15 / 0.47 flat across the 1M window, cache 0.016 / 0.20.

The other two rows this audit found have since landed through contributed
PRs, so only the annotations remain here:
- gpt-6-astra came in via #7162; this commit keeps its literal "12.5" and
  adds the note that low/medium/high/xhigh fall back to the base row.
- gemini-3.8-flash came in via #7164; this commit adds the note that
  0.75 / 3.75 / 0.075 is introductory through 2026-12-31 and reverts to
  the 1.50 / 7.50 / 0.15 list price on 2027-01-01.

Price changes (seed + repair, guarded on the previous values):
- gemini-3.6-flash: Google now lists the same 0.75 / 3.75 / 0.075
  introductory price through 2026-12-31.
- gpt-5.6-sol plus the bare gpt-5.6 row and its five effort-suffix rows:
  promotional 4 / 20 / 0.40 / 5, "available at least through November 21,
  2026". The alias rows must move in lockstep because exact-id lookup
  runs before suffix stripping.
- minimax-m2, minimax-m2.1, minimax-m2.5: the official pay-as-you-go
  page lists all three at 0.3 / 1.2 with cache read 0.03 and write 0.375.

The new repair entries are appended after the existing chain so old
databases hop through the intermediate prices (gpt-5.6-sol cache-write
backfill, minimax-m2.5 0.12 -> 0.15). The repair-order test now locks
both chains alongside the DeepSeek one.
2026-09-07 20:23:32 +08:00
Jason 2cd4006456 fix(codex): stamp requires_openai_auth on takeover writes to match the live login state
With official-login preservation off (the default), a direct switch to a
third-party provider deletes ~/.codex/auth.json. Enabling proxy takeover
afterwards rebuilt config.toml from the stored card, which still carried the
pre-0.149 `requires_openai_auth = true`, so Codex >= 0.149 trapped the TUI in
the login screen even though the proxy placeholder bearer token was in place.
The same gap hit hot switches after `codex logout` mid-takeover.

The takeover writer now stamps the active custom table's flag from the login
state Codex itself would observe, mirroring the direct-switch plan:

- Resolve the auth mode exactly like `AuthDotJson::resolved_mode` (explicit
  `auth_mode` first, then personal_access_token > bedrock_api_key >
  bedrock_access_keys > OPENAI_API_KEY > ChatGPT) before checking the
  credential, so Bedrock credentials next to a stale API key are never
  promoted to an OpenAI login (`account_state` would fail with
  UnsupportedBedrockApiKeyAuth and abort TUI startup).
- Decide `cli_auth_credentials_store` before touching auth.json: keyring and
  auto stores are undecidable from disk (the card's flag is left alone),
  ephemeral is always signed out, and only the file store reads the file.
  A missing, unreadable or corrupt file means signed out, as it does for
  Codex, and no longer fails the takeover write.
- Proxy-injected OAuth cards (xai_oauth, github_copilot) keep the neutralized
  `false` regardless of what is on disk; the official login is never their
  credential.

Official passthrough and managed-official branches are untouched.
2026-09-07 20:23:32 +08:00
Jason b1250dc7ac fix(tools): read Hermes latest version from GitHub Releases instead of PyPI
The tools panel asked PyPI for Hermes' latest version, but upstream
stopped publishing there after 0.19.0 (2026-07-20) and now ships only
through GitHub Releases. Users saw "latest 0.19.0" forever, often below
their installed version, and the upgrade button never appeared because
it only shows when latest is strictly newer than current.

PyPI was never the channel cc-switch installs or upgrades Hermes
through: install runs the official install.sh (git clone of main) and
update runs `hermes update` (git pull). Probe the same channel.

- Query GitHub Releases first and fall back to PyPI only when GitHub is
  unreachable or rate-limited.
- Parse the semver from the release name ("Hermes Agent v0.21.0
  (v2026.8.31)") since the tag is calendar-versioned; reject calendar
  numbers on both paths so the frontend never treats 2026.8.31 as a
  version and reports a permanent update.
- Hide the PyPI fallback value when the local version already leads it,
  so a degraded probe shows "unknown" rather than "latest < current".
- Add a 15s request-level timeout to the GitHub and PyPI probes; the
  shared proxy client allows 600s, which would stall the Hermes card
  and the refresh/upgrade-all buttons when api.github.com hangs.

Refs: #6475, #6618, #7033
Supersedes: #6621
2026-09-02 09:56:07 +08:00
Jason 4f62f676bd fix(presets): let the 9527CODE icon follow the theme
The nine 9527CODE presets pinned iconColor to #4F46E5, which came from
the docs site favicon rather than the logo itself. The vendor mark is
two-tone black (#000000 over #2D2D2D).

The fix is not to swap in #000000. ProviderIcon resolves colour as
preset iconColor -> metadata defaultColor -> undefined, and only the
undefined case leaves style.color unset, letting the inline SVG's
fill="currentColor" inherit the surrounding text colour. Any hex opts
out of theming, and a black one disappears in dark mode: the icon sits
in a bg-muted container at 18% lightness there, against a --foreground
of 98%, and nothing in the stylesheet inverts icons for dark mode.

Dropping iconColor and marking the metadata defaultColor currentColor
renders near-black on light, matching the official logo, and near-white
on dark. The traced SVG already suits this — its two tones are a
0.82-opacity base layer plus an opaque overlay, so the depth
relationship is relative and survives the inversion. Hard-coding both
tones would have foreclosed it.

This follows packycode and the 26 other currentColor icons.
2026-09-02 09:17:35 +08:00
Jason 68d71cc639 feat(presets): add 9527CODE presets across nine apps
9527CODE (9527.codes) is a sponsored enterprise-grade relay running New
API, fronting Claude, GPT, Gemini, Grok and Chinese models behind a
single key. It lands across Claude Code, Claude Desktop, Codex, Gemini
CLI, Grok Build, OpenCode, OpenClaw, Hermes and Pi, and is inserted
after APIKEY.FUN in both the preset files and the sponsor table so the
README order stays the ordering SSOT.

The gateway accepts every protocol with or without the /v1 prefix, but
the presets are written to match what each client actually appends
rather than relying on that fallback:

- Claude Code / Desktop / Gemini CLI:  9527.codes       (client adds /v1)
- Codex / Grok Build:                  9527.codes/v1    (responses)
- OpenCode:                            9527.codes/v1    (@ai-sdk/anthropic)
- OpenClaw / Hermes / Pi:              9527.codes       (anthropic-messages)

OpenCode and the other Anthropic-protocol clients look inconsistent but
are not: the AI SDK appends /messages to its baseURL while OpenClaw,
Hermes and Pi append /v1/messages themselves.

Three non-obvious details:

- endpointCandidates carry only the two live backups, api.9527.codes
  (overseas) and cdn.9527.codes (Asia-Pacific). The vendor docs list a
  fourth route, txcdn.9527.codes, which does not resolve; including it
  would offer users an endpoint that cannot connect.
- No cost is recorded for the OpenClaw and Hermes models. What a user
  actually pays is the official price times a token-group multiplier
  that varies per group and plan, so quoting list prices would mislead.
- The icon is an inline SVG traced from the vendor logo, drawn as a
  full silhouette at 0.82 opacity with the black regions layered on top.
  Stroking the two tones as separate adjacent paths leaves a hairline
  seam between them; the base layer covers it.
2026-09-01 23:47:14 +08:00
Jason cbbf727914 feat(presets): add AICodeWith presets across eight apps
AICodeWith (aicodewith.ai) is an aggregator exposing 30 models — Claude,
GPT, Gemini, GLM, Kimi, DeepSeek and Grok — behind a single key and
balance. It lands across Claude Code, Claude Desktop, Codex, Gemini CLI,
OpenCode, OpenClaw, Hermes and Pi.

The vendor exposes one endpoint family per protocol and its own docs warn
against reusing the generic /v1. All four are transcribed from the
official client guides:

- Claude Code / Desktop: api.aicodewith.ai             (Anthropic, no /v1)
- Codex:                 api.aicodewith.ai/chatgpt/v1  (responses)
- Gemini CLI:            api.aicodewith.ai/gemini_cli
- OpenCode:              api.aicodewith.ai/v1          (@ai-sdk/anthropic)

The first and last look inconsistent but are not: Claude Code appends
/v1/messages itself while the AI SDK appends /messages to its baseURL,
so both land on the same route.

Three non-obvious details:

- Claude Desktop uses mappedRoutes rather than passthroughRoutes. The
  upstream Haiku id is claude-haiku-4-5-20251001, which differs from the
  claude-haiku-4-5 role route id, so passthrough would point the Haiku
  route at a model that does not exist.
- OpenClaw, Hermes and Pi have no official integration guide. They reuse
  the Codex endpoint with their respective Responses protocol instead of
  guessing at /v1/chat/completions, for which the vendor publishes no
  example anywhere. Endpoint probing cannot settle this either: the auth
  middleware runs before route matching, so every path returns 401
  unauthenticated.
- Model ids come from GET /v1/models, which is unauthenticated and more
  current than the pricing page; the OpenCode guide still advertises
  kimi-k2 and glm-4.6, both retired.

The only logo the vendor ships is a 128KB PNG whose 2664 colours are
almost entirely compression noise over a two-tone image. It is vectorised
to a 4.9KB inline SVG using currentColor, so the icon follows the theme
rather than carrying an opaque white background.
2026-09-01 23:47:14 +08:00
Jason 6d25f34eaf feat(presets): add QwenCloud presets across seven apps (#6214)
QwenCloud is DashScope's international site — a separate deployment from
the existing Bailian presets, with its own domain, console and API keys.
It lands as new entries rather than a rename, so existing Bailian users
keep working unchanged.

Each of the three plans gets its own preset because their base URLs and
keys are not interchangeable; the vendor docs state that a mismatched
key/base_url pair fails with 401:

- Pay-as-you-go: dashscope-intl.aliyuncs.com
- Coding Plan:   coding-intl.dashscope.aliyuncs.com
- Token Plan:    token-plan.ap-southeast-1.maas.aliyuncs.com

Endpoints and model metadata are transcribed from the official client
docs. Two non-obvious details:

- The Coding Plan's OpenAI endpoint is a bare /v1 with no compatible-mode
  segment, and only supports Chat Completions, so its Codex preset is
  marked openai_chat rather than inheriting the Responses default.
- qwen3.8 models expose a 983616-token context window and accept only
  low/medium/xhigh reasoning levels, so the Claude Desktop Token Plan
  routes are deliberately not flagged 1M-capable.
2026-09-01 10:14:41 +08:00
Jason 21fda0ea01 test(codex): align grok-4.5 reasoning tier expectations with 4-tier presets
c08040e92 declared xhigh for the grok-4.5 xAI presets but missed the
test expectations, leaving main's frontend checks red and dragging
every PR's Frontend Checks down with the same two failures.
2026-08-31 18:02:21 +08:00
Jason 054673e0b8 fix(codex): reject 2^64 in whole-float integer rewrite
u64::MAX as f64 rounds up to 2^64, so the > guard let exactly 2^64
through; the saturating cast then produced u64::MAX, which rounds back
to 2^64 and passes the round-trip check - silently rewriting the value
off by one. Guard with >= instead.
2026-08-31 09:55:35 +08:00
Jason d05a11cc21 refactor(codex): drop unused namespace SSE restore helpers
create_namespace_restore_sse_stream and restore_sse_block lost their
last caller when the xAI response path moved to the sanitize rewrite
stream; delete them instead of shipping #[allow(dead_code)].
2026-08-31 09:55:35 +08:00
Jason 9e1100534b fix(codex): address xAI native Responses review findings
- Remap unknown request models before sanitizing, so a subagent SKU
  that lands on grok-4.5 gets its unsupported sampling fields stripped.
- Intersect union-branch required lists instead of unioning them, so a
  flattened oneOf does not force fields the chosen branch lacks.
- Let grok-prefixed request models bypass the catalog remap: real Grok
  SKUs the catalog has not caught up with must pass through.
2026-08-31 09:55:35 +08:00
Jason c08040e926 fix(codex): declare xhigh reasoning tier for grok-4.5 xAI presets
Probed 2026-08-30 against native /v1/responses: grok-4.5 accepts
low/medium/high/xhigh and rejects max with HTTP 400 "Invalid reasoning
effort". Codex does not clamp out-of-catalog efforts, so the preset
list must match what the upstream actually accepts.
2026-08-30 23:53:14 +08:00
Jason c88b00faf6 fix(codex): neutralize managed-OAuth official-auth fallback flag
The v3.20.1 config-only switch refactor extended the keyless safety gate
to all write paths, which broke switching to proxy-managed OAuth cards
(xAI Grok OAuth): they are keyless by design — the local proxy injects
the real token per request — yet their preset snapshot inherited the
pre-0.149 template's requires_openai_auth = true, so the gate refused
the switch with provider.codex.config.official_auth_fallback.

Fix the snapshot instead of exempting the gate: force the flag to false
on the active custom table for proxy-injected OAuth providers
(uses_proxy_injected_oauth: xai_oauth, github_copilot — codex_oauth is
deliberately excluded, the official login IS its credential). The
neutralization hooks into build_effective_provider_for_live so
preflight, normal writes, and proxy backup/takeover projections all see
the same safe shape, and 0.149 resolves the provider as unauthenticated
without ever reading auth.json. The OAuth preset now emits
requires_openai_auth = false at the source; stored cards self-heal via
backfill on the next switch.
2026-08-30 23:27:03 +08:00
Jason 3217f72596 docs(release): add v3.20.1 release notes 2026-08-28 08:19:45 +08:00
Jason 9485cf2f3d chore(release): v3.20.1 2026-08-28 08:19:45 +08:00
Jason 5ff199b5a9 fix(usage): align session sync card spacing and gate Sync Now to manual mode
The session sync card sat in the page-level space-y-8 stack while the
pricing/maintenance cards below use space-y-4 inside the Accordion,
leaving an inconsistent 32px vs 16px gap. Group the sync card with the
Accordion under a shared space-y-4 wrapper so all three cards align.

Also show the Sync Now button only when auto-scan is off: in auto mode
the background timer already covers scanning, so the manual entry point
is redundant there.
2026-08-27 23:35:02 +08:00
Jason f05e20336e fix(usage): report pinned-rewrite skips in session sync results
When an external truncation or rewrite pins the cursor to EOF, the
skipped range is permanently dropped (replaying it would double-count
entries whose detail rows were already pruned into the rollups). That
was only logged; manual sync showed plain success. Surface it through
the result's errors instead — deliberately not deferred_files, whose
semantics promise a retry next round that will never happen.
2026-08-27 23:35:02 +08:00
Jason f8d97348cb fix(usage): detect non-append rewrites and surface read errors in session scan
Follow-up hardening on the byte-cursor scan:

- session_log_sync gains last_tail_fingerprint (added to the still
  unreleased v17->v18 migration): a hash of the bytes just before the
  cursor boundary. On each incremental read the stored fingerprint is
  verified against the file, catching same-size or larger rewrites
  that a size check cannot see. The verification read doubles as the
  seek to the cursor and seeds the rolling tail buffer, so the append
  fast path costs no extra IO.
- truncation and fingerprint-mismatch rewrites now pin the cursor to
  the current EOF instead of rescanning from byte 0. A rescan would
  re-import entries whose detail rows were already pruned into the
  daily rollups, double-counting them forever; skipping the rewritten
  range loses at most externally-modified lines, which is the lesser
  harm. Appends after the pin resume normal incremental reads.
- mid-file IO errors are now reported through the sync result
  (errors + deferred count) instead of silently returning success;
  manual sync has no next round to quietly retry in.
2026-08-27 23:35:02 +08:00
Jason bcee61be43 perf(usage): incremental byte-cursor scan for Claude session logs
Replace the line-number cursor with a byte offset so unchanged file
prefixes are skipped via seek instead of re-reading whole files each
round (12MB active file: 6.04s full scan -> 9.3ms incremental), and
prefetch all sync cursors in one query per round across the five
file-based importers instead of one row lookup per file.

Details:
- session_log_sync gains last_byte_offset via a standalone v17->v18
  migration (v17 already ran on live databases; appending the column
  to it would leave them permanently missing the column)
- cursor only advances past complete (newline-terminated) lines; a
  parseable unterminated tail is imported but deferred, fixing the
  legacy bug where a half-written line was counted and then skipped
  forever once completed
- legacy line cursors are converted by skipping N lines byte-wise,
  never by full rescan: rollup_and_prune deletes detail rows that
  request_id dedup relies on, so a rescan would double-count pruned
  history into the rollups
- load_sync_cursors now returns Result and aborts the round on
  failure instead of falling back to an empty map (same rescan
  double-count hazard); Pi reports the error through its result
- on mid-file read errors the cursor keeps the old mtime so the next
  round actually resumes from the committed offset
- imports and cursor updates commit atomically in one transaction

Verified with a frozen snapshot replay (1017 files / 409MB): old and
new code produce identical aggregates.
2026-08-27 23:35:02 +08:00
Jason 092ea1f362 feat(usage): add auto/manual session scan mode toggle
Users reported that the constant background scanning of client session
logs (every 60s) was intrusive. Add a sessionAutoSyncEnabled setting
(default on, so existing behavior is unchanged) that switches session
usage collection between two modes:

- Auto (default): background timer scans client session logs as before
- Manual: background scanning stops entirely (including the startup
  scan pass); logs are only scanned when the user clicks the new
  "Sync Now" button on the usage dashboard

The toggle only governs scan timing. Proxy request accounting is
real-time and never reads session files, so it is unaffected, and the
startup cost backfill (which only patches existing DB rows) still runs.

Also wires up the previously unused sync_session_usage command as the
manual sync entry point, and adds usage.sessionSync.* strings in all
four locales.
2026-08-27 23:35:02 +08:00
Jason 270a4ff372 feat(usage): add OpenCode Go subscription usage query via token plan path
Query the official first-party (undocumented) endpoint
GET https://opencode.ai/zen/go/v1/usage — Bearer-only auth, the opposite
of the inference-side /messages which only accepts x-api-key. The 200
response shape was verified against a real Go subscription key.

- Map rolling/weekly/monthly windows onto the existing five_hour /
  weekly_limit / monthly tiers: no new i18n keys or tray label groups
- Parse each window defensively (upstream reshaped the response within
  an hour of launching it); report an explicit error when no window
  parses instead of rendering an empty card
- Drop resetsAt when percent is 0: the upstream value is a now+window
  placeholder, not a real reset time
- Treat 403 as "valid key but no Go subscription" (Zen and Go share
  the same workspace key), distinct from 401 auth failures
- Auto-enable the usage script on newly added providers across all five
  apps shipping the OpenCode Go preset; claude keeps its existing
  all-coding-plans behavior while claude-desktop/codex/opencode/pi only
  opt in for OpenCode Go
2026-08-27 08:37:56 +08:00