Commit Graph
2575 Commits
Author SHA1 Message Date
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
M3chD09 793e67d9b3 feat(provider): add Command Code presets and quota support (#7358)
- add Command Code presets for Claude Code and Codex
- map the Claude and Codex provider endpoints and model catalogs
- detect both base URLs and query credits, rolling limits, and subscription periods
- document the preset and usage coverage in English, Chinese, and Japanese
- add preset routing and quota parsing tests
2026-10-03 19:52:23 +08:00
小七andJason bfaaba1679 fix(proxy): strip inline <think>/<thinking> blocks on the Claude and Codex chat paths (#7741)
Chat-compatible upstreams (DeepSeek-style models, MiniMax M3) can inline
reasoning at the start of `content` as `<think>…</think>` or
`<thinking>…</thinking>`. The Claude path passed the whole block through
as plain text, tags included; the Codex path only knew `<think>`.

Split the leading block into reasoning with one shared splitter
(proxy/providers/inline_think.rs), used by the Claude and Codex paths,
streaming and non-streaming:

- Both tag pairs are recognized, mismatched pairs are tolerated, and tags
  split across SSE chunks are handled.
- Reasoning is emitted per delta; only a short tail that may be the start
  of a close tag is held back. The idle timeout measures the converted
  stream, so buffering the whole block until the close tag let it fire
  during long reasoning while the upstream was still sending. The Codex
  path had this problem since it first shipped.
- Held content is flushed at every boundary: tool calls, finish_reason,
  [DONE], truncated EOF and stream errors. At a tool call it is released
  before the tool block opens, so blocks never overlap.
- Newlines adjacent to a tag are separators and are not emitted, tracked
  across deltas so the output does not depend on SSE chunking. Spaces and
  tabs are kept, they may be indentation.

Fixes #7722
Fixes #7271

Co-authored-by: Jason <farion1231@gmail.com>
2026-10-02 22:41:37 +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
tingfeng347 4e46e6b630 fix(mcp): stop writing type into Codex mcp_servers (#7735)
Codex has no `type` key under `[mcp_servers.*]`; it infers the transport
from `command` (stdio) or `url` (streamable HTTP). Since Codex 0.158 the
`type` line CC Switch wrote on every MCP sync is reported as an
unrecognized setting, and `--strict-config` refuses to start.

Stop emitting the key from the shared Codex serializer while still using
the stored transport type to pick which fields to write. On import, infer
the transport from `url` when `type` is absent so url-only entries are no
longer treated as stdio and skipped. Grok Build output is unchanged.

Fixes #7734
Fixes #7554
Fixes #5820
2026-10-02 21:33:19 +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
Sailing Loong d0b57827e4 fix(codex): raise OAuth client identity to 0.159.0 for GPT-6.1 Sol (#7806)
Requests for gpt-6.1-sol through the Claude-to-Codex OAuth route advertised Codex 0.155.0, below the ChatGPT-account catalog gate for that model, so the backend rejected them with HTTP 400 "model is not supported when using Codex with a ChatGPT account" and model discovery omitted it. The gate sits above the model's advertised minimal_client_version (0.153.0): the catalog lists gpt-6.1-sol only from client_version 0.159.0. Bump the shared client identity to 0.159.0, the exact observed floor rather than the latest release.

The regression tests for the generation headers and the model-list request now assert a version at or above the GPT-6.1 Sol floor.

Fixes #7797
2026-10-02 08:26:06 +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
Xu Chang 9be1ef7e38 fix(pricing): add GPT-6.1 Sol pricing (#7800) 2026-10-01 18:49:56 +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
ikim 36d950411e fix(sessions): sort OpenCode V2 messages by seq and support mixed V1/V2 databases (#7765) 2026-09-30 12:29:58 +08:00
funkpopo 60cc14f886 fix(settings): 保留工具升级状态并优化错误提示 (#7764)
* fix(tool): improve tool action handling and user feedback for ongoing installations

* fix(about): enhance tool upgrade handling to preserve WSL parameters across remounts
2026-09-30 11:54:41 +08:00
funkpopo 19b7c17073 feat(opencode): 支持 JSONC 配置并保留注释与格式 (#7762)
* refactor: update OpenCode configuration handling and enhance JSONC support

- Refactored OpenCode configuration reading to use `read_opencode_config_from_path` for improved error handling.
- Added tests to ensure that OpenCode JSONC provider and MCP workflows preserve comments during import and export.
- Updated localization files to reflect changes in OpenCode configuration directory description, now supporting both JSON and JSONC formats.
- Introduced a new `jsonc_document.rs` module for format-preserving JSONC/JSON5 editing, including functionality for parsing, applying changes, and validating output.
- Enhanced existing tests to cover various edge cases in JSONC handling, ensuring robustness against invalid configurations and preserving comments.

* test(opencode): 添加对 Unicode 行注释的处理测试

* fix(opencode): 添加供应商和 MCP 服务配置格式错误的警告日志
2026-09-30 11:54:03 +08:00
funkpopo 482f57b335 fix(settings): serialize tool lifecycle writes and isolate Windows scripts (#7725)
Run each Windows tool lifecycle script from its own temp directory so concurrent calls can no longer overwrite or delete each other's .bat file.

Add a backend ToolLifecycleCoordinator: a global execution lock serializes all install/update writes (pnpm/bun share one global manifest, and concurrent 'add -g' silently drops one side's entry), and a per-tool try-lock rejects duplicate submissions even after the About page remounts. Locks move into the blocking task so a cancelled caller cannot release them while the installer is still running.

The About page now locks per tool instead of globally, queues multi-install confirmation plans, ignores double clicks on the confirm dialog, and shows the full error text in a tooltip.
2026-09-30 10:01:58 +08:00
ikim 74f4614340 fix(sessions): support OpenCode V2 SQLite schema in session manager (#7755)
OpenCode 2.x moves sessions into session_v2/session_message and stops writing the V1 tables, so the session manager missed every new session.

- Detect the V2 layout when both session_v2 and session_message exist (same rule as the usage collector)
- Scan, load and delete V2 sessions, including their child tables
- Reuse opencode_config::get_opencode_db_path() for the database path
2026-09-30 09:57:59 +08:00
Allen Xu 2cdfc6aebc fix(proxy): preserve terminal web search output order (#6509)
When a Responses SSE stream delivers its output only in the terminal response.completed / response.incomplete event, route it through the established Responses JSON conversion so text before a web search stays ahead of the search result. A missing terminal status falls back by event name, matching the incremental path's stop reason mapping. Also fixes terminal-only function_call items that previously produced stop_reason tool_use without a tool_use block.

Follow-up to #5681.
2026-09-30 09:11:27 +08:00
funkpopoandJason 38a7c9c479 fix(opencode): 支持导入和编辑内置供应商覆盖 (#7749)
* feat: Enhance OpenCode provider configuration and import functionality

- Added support for built-in provider defaults in OpenCode configuration.
- Updated OpenCode provider options to allow omitting npm and model definitions.
- Improved import logic for OpenCode providers from live config, ensuring existing credentials are respected.
- Introduced tests for credential handling and built-in provider editing.
- Enhanced UI components to reflect changes in provider configuration and selection.
- Added localization for new built-in defaults feature.

* refactor(tests): enhance test environment isolation and settings management

* Refactor OpenCode provider handling and validation

- Removed TestSettingsGuard to simplify settings management during tests.
- Enhanced validation in handleDuplicateProvider to ensure npm and model definitions are provided for non-OMO providers.
- Updated OpenCodeFormFields to conditionally allow built-in defaults based on props.
- Improved ProviderForm to manage existing OpenCode overrides and prevent incomplete copies from being added.
- Added comprehensive tests for OpenCode provider duplication and validation scenarios.
- Introduced new error messages for better user feedback on provider requirements.
- Created opencode_tests.rs to cover various scenarios for OpenCode provider imports and updates.

---------

Co-authored-by: Jason <farion1231@gmail.com>
2026-09-30 07:55:43 +08:00
Bryan Nie f678f7c539 fix(opencode): preserve provider fields during import and sync (#7577)
Import and live writes now persist the original provider JSON and use OpenCodeProviderConfig only for validation and display-name extraction. The typed round trip dropped fields the type does not model, such as api, env, whitelist and models.<id>.limit.input. Removes the lossy get_typed_providers/set_typed_provider helpers.

Refs #7382
2026-09-30 00:22:34 +08:00