Commit Graph
100 Commits
Author SHA1 Message Date
Jason 4f8d1424a6 feat(tray): rebuild the tray menu around apps and their modes
The native menu now lists Claude Code, Claude Desktop, Codex, Gemini CLI
and Grok Build (hidden apps are skipped; additive apps stay out). Each
app row reads "name · mode · provider · quota" and opens a submenu that
states the mode and only offers providers that switching can actually
use: providers that need routing open the app page with the same
needs-routing dialog, official subscriptions are disabled under routing,
and the menu is read-only while failover is on instead of silently
turning failover off. Projects move into the app submenus.

Quota text matches the cards (remaining, "running low" under 10%, no
emoji). A problems section appears only when something needs action:
the routing service is down while apps use it, an app fell back to
direct at startup, or a tray switch failed.

On Windows a left click opens the window and a right click shows the
menu; macOS and Linux keep the menu on left click.
2026-10-03 23:28:58 +08:00
Jason b999b07bec feat(ui): rebuild the global pages and finish the v7 visual pass
Pages (phase 4):
- Usage: v7 layout (filter row, four metrics with more on demand,
  switchable trend, request log / provider / model / pricing tabs, data
  sources sheet). Latency becomes output speed: output / (duration -
  first token), shown only for requests with at least 100 output tokens;
  provider totals sum output and generation time instead of averaging.
- Sessions: single-column list grouped by project or time, drilling into
  a reader with resume, find, question jump and copy actions.
- MCP and Skills share one app matrix (neutral checks, retry on failed
  writes, column bulk actions with undo); MCP add/edit moves into a
  drawer with form/JSON modes; Skills merges installed and discover and
  always shows a Pi column.
- Prompts: one page for eight apps including Hermes SOUL.md, enable and
  disable as buttons, delete with undo instead of a confirm, copy to
  other apps, import existing files; app pages show the active prompt.
- Add provider is two steps: pick a preset (always-on search including
  Chinese names and domains, categories inferred for display, sorted by
  name with no promoted entries), then fill the form. Edit pages name
  the provider and warn when it is the one in use.
- App page menu links to that app's usage.

Visual pass (phase 6):
- Secondary pages cover only the content area so the sidebar stays.
- Cmd/Ctrl+K jumps to apps, pages and settings sections.
- Legacy button variants, primary color, dialogs, toasts and sheets
  follow v7 tokens; hard-coded palette classes and 10px text across
  the remaining components are replaced with tokens.
2026-10-03 23:28:58 +08:00
Jason a0b722c349 feat(ui): show quota as what is left, quiet unless running out
Quota on provider cards now reads as remaining ("5-hour 69% left", "Balance 82.10") in the secondary text color, keeping the two tiers with the least left. A tier under 10% turns warning, used up / expired / failed turns danger, and a failed query shows the reason on a second gray line. Reset times and the last update move into the hover text; clicking the quota checks again. Expanded views use one bar style where a shorter bar means less left. Green is gone from quota everywhere.
2026-10-03 23:28:58 +08:00
Jason 4aab608f7c feat(ui): add the v7 mode layer to provider pages and routing settings
Switch-style apps (Claude Code, Codex, Gemini CLI, Grok Build) get a mode
row (Direct / Routing / Stack). Tabs only change what is viewed; taking
effect goes through buttons that state the consequence, with a confirm
dialog for entering a proxy mode and a one-step, undoable return to
direct. Provider cards are drawn from a per-mode presentation model
(status, buttons, chips, queue order, disabled reasons) instead of each
card inspecting proxy state, and additive apps get Added / Available
sections.

Backend: entering a mode can pick the route target in the same step,
`get_app_mode` reports the effective mode per app, and startup attach
failures are kept for the UI to surface with a retry.

Settings gains a Local Routing section (service, listen address, apps
using routing, failover tuning) replacing the old proxy tab and toggles.
Claude Desktop gets an access bar for model mapping and service state.
2026-10-03 23:28:58 +08:00
Jason 40aa2864a2 feat(ui): move to a sidebar shell with global pages and grouped settings
Replace the header toolbar and icon-only app switcher with the v7 shell:

- Sidebar (200px, collapses to a 72px icon rail under 960px, Cmd/Ctrl+\
  toggles and remembers the choice): the app list with mode tags and
  "needs attention" dots, then Usage (today's cost), Accounts (re-login
  dot), MCP, Skills, Prompts and Sessions pinned to the bottom, and an
  Apps / Settings footer. Settings swaps the sidebar for its own
  directory.
- Every screen gets a 52px page header; Linux in-app window controls
  sit at its far right. macOS traffic lights move down to centre in the
  44px sidebar top bar.
- Usage, Accounts, MCP, Skills, Prompts (with an app picker) and
  Sessions become global pages instead of settings tabs or per-app
  toolbar buttons. OpenClaw and Hermes switch to their own pages with a
  segmented control; OpenClaw's env, tools and agents merge into one
  Config page.
- New Apps page: per-app install / upgrade with path and install
  source, batch upgrade, conflict details, manual commands, and the
  "show in sidebar" switch that used to live in settings. Tool
  management moves out of About into a shared hook; a read-only
  list_tool_installations command feeds the page so it does not share
  calls with the upgrade preflight.
- Settings regroup into General, App config, Local routing, Network,
  Data and About, using row/card layout. Path settings save per
  section; switches still save immediately. Skills storage and sync
  move to a sheet on the Skills page.
- New opt-in "check app updates on startup" setting (off by default):
  when on, a background check puts a neutral dot on Apps.
- Switch adopts the v7 track and action colour; env-conflict banner
  joins the page flow instead of overlaying the header.

The old route / Stack / failover toggles stay in the provider page
header until the mode tabs land.
2026-10-03 23:28:58 +08:00
Jason 947a8a8b06 feat(ui): add v7 design tokens and shared primitives
Foundation for the v7 redesign; no layout changes yet.

- Add the v7 tokens (surfaces, text, action, inverse, direct / route / stack,
  status, chart, shadows) to index.css and expose them in Tailwind together
  with v7 radius, shadow and type-role scales; teach tailwind-merge the new
  names so text-body no longer cancels text-fg-1.
- Align the shadcn HSL tokens (background, card, muted, border, ring) with the
  v7 palette and use the neutral ring for focus outlines.
- Rename light / dark to Light / Dark Side of the Moon with System first, and
  pause transitions while switching so colors don't fade in piece by piece.
- Add v7 button variants (solid, neutral, quiet, direct, route, stack) and
  sizes, plus HelpTip / DisabledReason (the "?" card), SegmentedControl,
  Notice / NoticeSlot and Sheet.
- Respect the OS reduced-motion setting for framer-motion.
2026-10-03 23:28:58 +08:00
Jason 98798552c0 fix(proxy): keep the routing service running for Claude Desktop model mapping
Claude Desktop model-mapping providers send every request through the local
routing service, but the service lifecycle only counted the four routing apps:
when Claude Code left routing, stop_server_if_unused stopped the service Desktop
was still using, and nothing started it on launch.

- stop_server_if_unused now also treats a current Desktop model-mapping
  provider as a user of the service.
- ensure_desktop_mapping_service starts the service when that provider is in
  use; it runs on startup, after switching a Desktop provider and after
  applying a project that covers Claude Desktop.
- The frontend no longer warns that routing must be started when switching to
  a Desktop model-mapping provider, since the backend starts it; drop the now
  unused isProxyRunning hook argument and proxyReasonClaudeDesktop string.
2026-10-03 23:28:58 +08:00
Jason cee903476d fix(ui): surface usage and accounts entries, show scrollbars and card switch buttons
- Always show the usage button in the header and add an Accounts button; the
  usage dashboard has data from session logs even without routing.
- Cmd+, now always opens the General settings tab.
- Remove the dead agents and universal views, which had no entry point but
  could still be restored from localStorage.
- Restore thin always-visible scrollbars; keep a .no-scrollbar opt-out.
- Keep the provider card's main action visible; only the secondary icon
  buttons still reveal on hover.
- Show the needs-routing badge for every app providerNeedsRouting covers,
  which adds the missing Grok Build badge.
- Replace leftover "proxy takeover" wording with Routing mode and refresh the
  outdated Accounts description in all four locales.
2026-10-03 23:28:58 +08:00
Jasonandh6rime ad368eb7ff fix(sessions): read Hermes state.db transcripts the way Hermes displays them
Opening any Hermes session in the Session Manager failed with "no such
column: created_at": the messages table stores `timestamp` (REAL epoch
seconds). Rework the SQLite reader to follow Hermes' own display
projection:

- Select columns by what the store actually has, so older state.db files
  without active/compacted/display_metadata still load.
- Show live rows plus compaction-archived rows (active = 1 OR
  compacted = 1); rewound/regenerated rows stay hidden. Collapse the
  copies compression re-inserts with Hermes' display identity (role,
  content, timestamp, tool_call_id, tool_calls, tool_name), keeping the
  first position and the most live copy, and skip model_only rows.
- Order by id: timestamps regress across clock skew and compaction.
- Decode "\0json:" structured content (text + image parts) to text.
- Render assistant tool calls as [Tool: name], matching Codex.

The session list now uses the first user message as summary (and as the
title when Hermes has none) and prefers last_activity_at for recency. The
provider filter dropdown gains a Hermes entry.

Supersedes #6694.

Co-authored-by: h6rime <127593600+h6rime@users.noreply.github.com>
2026-10-03 23:28:58 +08:00
JasonandAnxForever 81c0ff252b fix(mcp): keep projecting remaining servers when one fails to sync
Projecting an app's MCP servers stopped at the first server that could
not be written or removed, so one bad entry left every server after it
out of sync: newly enabled ones were never written and disabled ones
stayed in the client's config.

Sync each server independently, log the failures, and report them
together at the end, grouping server ids that failed for the same
reason.

Co-authored-by: AnxForever <130662349+AnxForever@users.noreply.github.com>
2026-10-02 22:33:33 +08:00
JasonandChrisPan 27e5282235 fix(mcp): write url-only Codex MCP specs as HTTP and keep transport-only fields apart
A spec without `type` was always written as stdio. The panel's JSON mode
and deeplinks both accept a bare `{"url": …}`, so such a server ended up
as `command = ""` plus `url`. Codex rejects that entry with "url is not
supported for stdio" and refuses to load the whole config.toml, taking
every other MCP server down with it.

Infer the transport on the write side the same way the import side does:
no `type`, or an explicit stdio with no command, is HTTP when a `url` is
present.

Codex applies the same all-or-nothing check to every field that belongs
to the other transport, so write only the chosen transport's fields.
Stdio entries no longer carry `url`, `http_headers`, `env_http_headers`,
`http_headers_helper`, `bearer_token_env_var` or `oauth_resource`; url
entries no longer carry `command`, `args`, `env`, `env_vars` or `cwd`.
Each field is still written where it is valid. The lists were checked
field by field against Codex 0.160.

Refs #6719

Co-authored-by: ChrisPan <39005916+szupzj18@users.noreply.github.com>
2026-10-02 22:33:33 +08:00
Jason b9e9620265 chore(presets): remove ETok sponsor slot and presets
Drop the ETok.ai entry from the four-language README sponsor table and
remove its presets from all nine apps along with the partnerPromotion.etok
copy. The etok icon stays in the icon index so existing user providers
that reference it keep rendering.
2026-10-02 11:43:11 +08:00
Jason e88437e711 fix(usage): stop output TPS from spiking on late first-token marks
first_token_ms was stamped when the usage collector accepted its first
event, so the usage filter decided what "first token" meant. When an
upstream's opening event carried no usage key (Responses passthrough
without usage on response.created, Chat passthrough with usage only on
the final chunk), the first accepted event was the terminal one, the
generation window shrank to a few milliseconds and TPS jumped into the
thousands.

Mark the first output on every SSE data line instead, independent of the
usage filter, skipping only metadata events (response.created,
response.in_progress, ping, keepalive), the same semantic TTFT rule
Sub2API uses.

On the frontend, hide TPS when the generation window is under 100ms:
relays that buffer and flush at once, or short replies landing in one
packet, measure a transport burst rather than generation speed. This
also cleans up rows recorded before the backend fix.
2026-10-02 09:10:57 +08:00
Jason 67d1daa1af fix(subscription): silence non-macOS dead-code lint on Codex keychain variants
CodexKeychainLogin::Found and ::Missing are only constructed by the macOS keychain reader, so non-macOS lib builds never construct them and clippy -D warnings fails on Linux and Windows. Allow dead_code on those two variants outside macOS only, keeping the lint active on macOS.
2026-10-02 09:02:53 +08:00
Jason 1bc68e293f fix(proxy): reject Stack model ids outside Stack mode
Stack ids are only resolved while the app is in proxy mode with Stack
mode on. In routing mode the list and keys are kept but not published,
so a client still holding an old Stack id gets an error instead of being
forwarded to a list member. The Stack bit is left as is when leaving
proxy mode, so direct mode is gated too.
2026-10-01 23:55:12 +08:00
Jason 850ba952df feat(i18n): call Stack mode 叠加 in Chinese copy
Rename the Stack mode to 叠加 (zh-TW 疊加) in the Chinese UI so it reads
alongside 路由 and 映射. Tags and tabs say 叠加; sentences say 叠加模式
or 叠加的模型.

- zh / zh-TW locale strings, the form defaultValues and the matching
  test assertions
- the Chinese half of user-facing backend errors in mode/controller.rs,
  mode/stack.rs and codex_direct.rs; the English halves are unchanged

English and Japanese keep "Stack". Code identifiers, persisted keys
(enableStackMode, the live-state stack mode), commands and log text are
unchanged.
2026-10-01 23:45:51 +08:00
Jason c6255cc91d fix(codex): mirror official entries for GPT models in the model catalog
Once `model_catalog_json` is set, Codex only knows the models in that file.
Third-party GPT rows were built from the neutral native template (a one-line
"You are Codex" prompt, `model_messages` stripped) or, on ProxyChat, from a
gpt-5.5 clone regardless of the model name, so GPT models lost their own
harness prompt, freeform apply_patch and reasoning levels.

Rows now look up Codex's bundled official list with Codex's own matching rule
(longest prefix, then one simple namespace segment, case-sensitive) and mirror
the matched entry verbatim; row overrides do not apply, and a narrower relay
window goes in `model_context_window` in config.toml. Fields owned by the
official account or backend are reset: Responses Lite off, speed tiers,
upgrade and NUX cleared, always listed, and ProxyChat keeps
`supports_image_detail_original` off. Anthropic rows and the DeepSeek vendor
catalog are unchanged. Stack members get the same entries under their
prefixed ids.

The reverse parser only collapses a row to its bare model name when it
regenerates identically, so older ProxyChat gpt-5.5 clones keep the user's
display name and window.
2026-10-01 23:45:51 +08:00
Jason d46478e8e0 fix(codex): stop warning when the managed OAuth marker is absent
A native ChatGPT login has no managed-account marker, which is the normal
state, but the ownership check logged "Failed to read Codex managed OAuth
auth marker ... file does not exist" on every switch and token lookup.
Check that the marker exists first, as the other marker readers in the
same file do. A marker that exists but cannot be read still warns, and
the result is unchanged: no marker never establishes ownership.
2026-10-01 23:45:51 +08:00
Jason 85bd880195 fix(codex): show and save keyless drafts in the provider editor
Opening the Codex add dialog projects the custom template onto the live
config to show what config.toml will look like. The template has an empty
API key and `requires_openai_auth = true`, and the projection ran the same
check a switch does, so the dialog failed with "this Codex config has no
usable API key" before the user could type one. Third-party presets with
the same defaults failed the same way. The editor base then stayed empty,
so saving fell back to storing the row only and global edits made in the
dialog never reached the live config.

The editor now projects such a draft as if a key were present, using an
in-memory placeholder that is removed before display and before the row
is stored. Only the missing-key error is retried; other errors surface as
before. A keyless row can be saved, as the non-editor add path already
allowed once a current provider exists. Every write to the live config
still projects the real row and refuses it: switching or routing to it,
editing the current provider in direct mode, and adding it as the first
provider, with the row rolled back.
2026-10-01 23:45:51 +08:00
Jason 619c315b8d fix(codex): keep proxy sessions in the same history bucket as direct
Codex groups sessions by the model_provider id in config.toml: the CLI's
resume picker and `resume --last` only list sessions recorded under the
current id, and resuming a thread needs its recorded id to still resolve.
With the official provider as the proxy (or Stack) route, CC Switch wrote
`model_provider = "cc-switch-official"`, so those sessions went into a
third bucket, separate from both the official (`openai`) and third-party
(`custom`) history, and the table was deleted as soon as the route moved
away.

The official route now uses the same id as direct official use:
- Unified session history off: no model_provider; a top-level
  `openai_base_url` points the built-in openai provider at the local proxy,
  so sessions stay under `openai`. A leftover custom table is kept in its
  dormant form, as before. The key is updated in place, not moved.
- Unified session history on: the custom official mirror table, pointed at
  the proxy with websockets off.
The two shapes produce different contracts, so toggling the setting while
routed to the official provider rewrites the client. `cc-switch-official`
is no longer written; an existing table is only removed.

`openai_base_url` is written only for this route and only with the local
proxy address. The official route already sent the client's own login to
the proxy, so nothing new is exposed. Direct third-party routes still never
use it.

Supporting changes:
- The proxy answers GET on the Responses paths with 426. The built-in
  openai provider tries a WebSocket first and falls back to HTTP for the
  session right away on 426; other errors used up its retries first.
- The built-in provider's `version` header is stripped from requests sent
  to third parties, along with the other Codex client headers.
- The new shapes carry no placeholder key, so `live_has_proxy_placeholder`
  also recognizes a live config that routes the official provider to the
  configured proxy address. The port must match; with port 0 only the host
  and path are checked. Other local gateways that point openai_base_url at
  a different port are not treated as leftover takeover state.
2026-10-01 23:45:51 +08:00
Jason 5a788d8717 feat(proxy): remind to restart Claude Code when Stack models change
Claude Code fetches the model list from the proxy once at startup, so a
change to the published Stack models only shows up after a restart.
Adding or removing a member already said so in its saved toast, but
editing a member's models or deleting a member did not.

In Claude Code Stack mode the provider list now compares the published
model ids each time the Stack list is refetched and shows "Stack models
changed. Restart Claude Code to apply" when the set differs. The refetch
right after adding or removing a member is skipped, since that toast
already asked for a restart; the skip is used up by that refetch even
when nothing changed, so it cannot swallow a later edit. Reordering
models or editing fields that are not published (keys, addresses) does
not trigger it. Codex is left to its process-based banner.
2026-10-01 23:45:51 +08:00
Jason 52e0fa8a6c feat(proxy): warn when Codex clients hold an old Stack model list
Codex's app-server reads the model catalog once at startup and keeps it,
while config.toml is re-read per request. Since 0.159 the codex CLI talks
to a managed daemon that only restarts on upgrade, so reopening codex after
a Stack change still shows the old model list; the desktop app and editor
extensions have the same problem with their own app-servers.

In Codex Stack mode the provider list now shows a banner when a running
Codex client started before the current catalog was written:
- CLI daemon: a button restarts it through Codex's own
  `codex app-server daemon restart`, after a confirmation that running
  tasks will be interrupted. Nothing runs when the daemon is not up.
- Desktop app / editor extensions: the banner explains how to fully quit
  and reopen them; CC Switch does not restart them.

Staleness is judged against a device-local history of catalog generations
(codex-catalog-history.json) rather than file times: an unchanged catalog
is not rewritten, and quitting CC Switch removes the catalog pointer and
restores it on the next start, so a daemon that survived a CC Switch
restart still has the current catalog. Every Codex write and startup
records the catalog a newly started Codex would read; processes that
started before the oldest recorded generation count as stale.

Processes are read with ps on macOS and Linux only; Windows shows no
banner for now.
2026-10-01 23:45:51 +08:00
Jason 5990d144c3 feat(proxy): highlight Stack members in violet
In Stack mode every added provider now stays highlighted like an added
provider in additive apps. The card and the Stack badge use violet,
matching the Stack icon in the header, so Stack mode is easy to tell apart
from routing and failover, which stay green.
2026-10-01 23:45:51 +08:00
Jason 0123011228 feat(proxy): point Claude Code aliases at the default's first Stack model
In Stack mode the default provider no longer hides behind the routing
aliases. Claude Code's four role aliases (startup default, background
tasks, subagent aliases) all point at the first model of the default
provider's list, as its Stack id. Haiku drops the [1M] marker like the
routing contract does, and CLAUDE_CODE_SUBAGENT_MODEL is not written so
subagents follow the model picked in /model. The default provider's
whole list is published like any other member; Codex keeps publishing
the default's catalog as the route's own rows.

Claude providers get an explicit Stack model list (meta.stackModels).
Unset means the list follows the model mapping (ANTHROPIC_MODEL first,
then the tiers); an empty list is a deliberate choice and publishes
nothing.

In Stack mode the provider form opens a simplified layout: the model
list with a star for the default, plus the fields needed to reach the
provider, with a link to the full form. Both layouts share one form
state, so the default is synced when the user acts, not derived from
the layout at save time:
- Claude: when the first model of the list changes, ANTHROPIC_MODEL
  follows it; an unchanged first model leaves ANTHROPIC_MODEL alone.
- Codex: the star follows config.toml's model. Starring, renaming or
  removing the default row updates model on the spot; save only fills
  model from the first row when it is empty. A default outside the list
  is flagged with an action to add it.
2026-10-01 23:45:51 +08:00
Jason b925c2bbf7 refactor(proxy): rename attached models to Stack mode
The mode that publishes several providers' models in one client model list
is now called Stack in both the UI and the code. The feature is unreleased,
so persisted names change without aliases: the live-state entry, the pending
op, the settings flag and the two commands now use "stack".

The proxy's own "attached" state (attached_route, mode.attached, op::ATTACH)
keeps its name; it means the proxy contract is in place, not this mode.
2026-10-01 23:44:38 +08:00
Jason 81cc2f7baf feat(proxy): carry Codex compaction and turn state across attached providers
When the official ChatGPT route is the default, Codex treats every model
under its "OpenAI" provider table as capable of remote compaction (V2): a
normal /responses stream ending in a compaction_trigger, which must return
exactly one compaction item or Codex fails the turn. Attached third-party
models could not do that, and history mixed across providers carried state
that the other side rejects.

Compaction bridge:
- Chat and Anthropic conversions and native Responses passthrough turn the
  trigger into a tool-free summary turn and return the summary as a single
  compaction item wrapped as ccswitch-compaction-v1.
- Only a normal stop counts as a finished summary. Truncated or filtered
  turns report response.incomplete; empty summaries report response.failed.
- On replay, CC Switch summaries become a user message for every upstream.
  Foreign blobs become a readable note for Chat/Anthropic conversions and go
  to native Responses upstreams untouched, since they may have issued them.

Official passthrough: before the ChatGPT backend, drop reasoning items that
carry CC Switch envelopes or no encrypted content, and remove item ids that
do not match their type. Pure official history stays byte-identical.

Opaque state rectifier: when a native Responses upstream rejects encrypted
state it cannot verify (invalid_encrypted_content, "could not be verified",
"not issued to this caller", store:false lookups, xAI blob errors), strip
reasoning items and encrypted parts, plus compaction where warranted, and
retry the same provider once. A second failure goes through the normal
error classification. Governed by the rectifier master switch.

Also strip x-oai-*, x-openai-* and x-responsesapi-* client fingerprint
headers from third-party requests, and drop stale entity headers when a
streamed SSE body is rewritten (this also fixes the xAI namespace restore
stream, which kept the upstream Content-Length).
2026-10-01 23:44:38 +08:00
Jason d3acdc2077 feat(proxy): make attached models a mode of its own next to routing
Attached models were a per-card toggle inside routing mode. Turn them
into an attached mode that sits beside routing: settings show either
the routing switch or the attached-mode switch on the main page, for
Claude Code and Codex only (Gemini CLI and Grok Build keep routing).
Turning one on first sends Claude Code and Codex apps that are in the
other mode back to direct.

In attached mode the provider list works like the additive apps: Add
and Remove put a provider's models into the client's model picker, and
"Set as default" picks the provider for unprefixed requests. The
default is always on the list and cannot be removed; a new default set
from the card, the tray or a project joins it. Official accounts can
only be the default. There is no failover: default requests go to the
default only, enabling failover is refused under the switch lock, and a
late failover result from a request sent before entering attached mode
is dropped. The queue is kept for routing mode.

Internally attached mode is proxy mode plus PoolState.enabled, written
on every proxy entry and committed with the client files. With the bit
off nothing is published and the list is kept for next time.

Also fix issues found reviewing the first two commits:
- strip ChatGPT fingerprint headers only from Codex attached requests
- recompute the Codex catalog when an attached member's row is edited
- skip a member whose row cannot be projected instead of failing every
  write, and refuse to attach such rows
- keep the gateway discovery switch as an exclusive field so a user's
  global value survives direct switches
- tell the user when the route or config.toml owns the model catalog
- read the Codex keychain entry by account key with a three-state
  result, outside the app lock and off the async runtime
- retry the Codex rewrite on the next check when a refreshed official
  list could not be written
2026-10-01 23:44:38 +08:00
Jason 73c187bee1 feat(proxy): attach third-party models to Codex in proxy mode
Merge attached providers' models into the Codex catalog as
ccs-<key>/<model> rows after the route's own rows. Each row keeps its
provider's tool profile, a fixed comp_hash and its own window; the
global window keys move into the rows while models are attached.

Attached requests keep their model, never carry the ChatGPT identity
headers to a third party, and drop hosted web_search for native
Responses gateways that reject it.

With the official account as the route, the catalog lists every
official model: fetched with the login Codex will use after the
operation, cached per workspace, user and Codex version, refreshed in
the background, with Codex's bundled list as the fallback. Logins CC
Switch does not manage are only read, never refreshed.
2026-10-01 23:44:38 +08:00
Jason 47d062fb9f feat(proxy): attach third-party models to Claude Code in proxy mode
Let proxy mode publish extra providers' models to Claude Code's /model
picker as ccs-claude-<key>--<model> ids. A request that selects one goes
straight to that provider and never touches routing state: no breaker
permit or result, no status counters, no in-use update, no failover
switch, no model mapping, single-provider timeouts.

The list and a permanent key registry live in live-state.json and are
committed with the client file in one operation; a failed change reports
whether it was dropped or will be rolled forward. Unresolvable prefixed
ids are rejected instead of falling back to the route. /v1/models serves
the Anthropic-shaped list for gateway model discovery.
2026-10-01 23:44:38 +08:00
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