2379 Commits

Author SHA1 Message Date
jintonglu6688
c58a25b2ae fix(a11y): add accessible names to GUI controls (#7049)
Give icon-only back buttons, the header local-routing switch, the project
switcher popover, the Claude JSON editors, and the existing-Skills import
checkboxes programmatic names so screen readers announce their purpose.
Reuses existing i18n strings where possible and adds two short localized
labels across zh/en/ja/zh-TW.

Fixes #7048
2026-09-02 12:57:56 +08:00
Eureka
460aa8c73d fix(pricing): seed Claude Fable 5.1 / Mythos 5.1 and restore Sonnet 5 to the $2/$10 standard price (#7051)
- Add claude-fable-5-1 / claude-mythos-5-1 pricing rows ($10/$50, cache read $0.25 = 0.025x, cache write $12.50). Without a row these requests were stored at $0: the prefix rule only looks for longer dated ids and cannot fall back to claude-fable-5.
- Sonnet 5: Anthropic's pricing page now states the $2/$10 introductory price is the standard price and the September 1 increase to $3/$15 will not occur. Update the seed and add a repair_current_model_pricing guard so installs seeded at 3/15/0.30/3.75 are corrected; user-customized rows are left alone.
- Tests: seed values for the three rows; the repair guard fixes the old list price and keeps a custom price.

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

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

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

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

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

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

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

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

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

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

Three non-obvious details:

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

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

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

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

Three non-obvious details:

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

The only logo the vendor ships is a 128KB PNG whose 2664 colours are
almost entirely compression noise over a two-tone image. It is vectorised
to a 4.9KB inline SVG using currentColor, so the icon follows the theme
rather than carrying an opaque white background.
2026-09-01 23:47:14 +08:00
cicicifang
e4b03a38c7 fix(usage): stop deferring resumed Codex rollouts on filename/meta ID mismatch (#6905)
* fix(usage): stop deferring resumed Codex rollouts on filename/meta ID mismatch

Codex resume creates rollout files named rollout-<ts>-<threadId>_<sessionId>.jsonl
(dual UUID). The root session_meta inside keeps payload.id = the ORIGINAL thread
id (the UUID before the underscore; continuation info lives in history_base).
The consistency check in parse_codex_file compared meta id against only the
filename's trailing UUID (the new session id), so every resumed rollout was
permanently deferred with:

  文件名线程 ID (<sessionId>) 与 root meta ID (<threadId>) 不一致

Usage statistics then silently stop: once the user resumes an old thread, all
subsequent turns append to the dual-ID file which is skipped every sync cycle.
Reproduced live: stats stopped at 08/27 17:33, file had 152 unimported
token_count events from that day, deferral warning repeated every 60s cycle.

Fix: also accept meta id matching the pre-underscore UUID of dual-segment
filenames (leading_thread_id_from_filename). Single-UUID filenames keep the
strict check. thread_id_from_filename still returns the trailing UUID - it is
the request_id prefix (codex_thread:<id>:<event_index>), and event_index is
per-file, so the trailing session id is what keeps dedup keys from colliding
across segments of the same thread.

Tests: resumed rollout with meta id = leading UUID no longer defers (asserts
parent != Deferred and root_thread_id = trailing UUID); single-UUID mismatch
still defers. 2576 pass, clippy clean.

* fix(usage): store session_id as the leading thread id for dual-segment rollouts

Dual-segment rollout filenames carry <threadId>_<rolloutId>: the leading
UUID is the stable logical thread id (what session_meta.payload.id holds),
the trailing one is the rollout id of the replacement physical file. Such
files are minted by thread/revert (see openai/codex#38127); subsequent
resumes append to them. The sync path was writing the trailing rollout id
into proxy_request_logs.session_id, while the session manager keys threads
by the meta id - so usage rows of a reverted thread attach to an id that
never shows up in the session list.

No query consumes session_id today, so nothing is visibly wrong yet; but
the misattributed rows keep accumulating, and correcting them later would
require rescanning rollout files to rebuild the mapping - unrecoverable
once users delete those files.

Fix: parse_codex_file captures the normalized root meta thread id and the
insert path uses it as session_id. The trailing rollout id keeps its only
remaining role as the request_id prefix (codex_session:thread-v1:<id>:<n>;
event_index counts per file, so the trailing id is what prevents dedup-key
collisions across segments of one thread). Single-segment files are
unaffected: meta id equals the filename uuid there.

Tests: full-sync regression on a dual-segment rollout asserting request_id
contains the trailing rollout id while the stored session_id equals the
leading thread id.
2026-09-01 22:01:08 +08:00
thisTom
92a9b4a91d fix(usage): compact trend token axis labels (#7016)
Co-authored-by: thisTom <19346741+thisTom@users.noreply.github.com>
2026-09-01 12:32:06 +08:00
Jason
6d25f34eaf feat(presets): add QwenCloud presets across seven apps (#6214)
QwenCloud is DashScope's international site — a separate deployment from
the existing Bailian presets, with its own domain, console and API keys.
It lands as new entries rather than a rename, so existing Bailian users
keep working unchanged.

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

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

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

- The Coding Plan's OpenAI endpoint is a bare /v1 with no compatible-mode
  segment, and only supports Chat Completions, so its Codex preset is
  marked openai_chat rather than inheriting the Responses default.
- qwen3.8 models expose a 983616-token context window and accept only
  low/medium/xhigh reasoning levels, so the Claude Desktop Token Plan
  routes are deliberately not flagged 1M-capable.
2026-09-01 10:14:41 +08:00
Xinchen Lee
273c9cc24f fix(codex): mark glm-5.3 as text-only (#6851) 2026-08-31 23:00:26 +08:00
Jason
21fda0ea01 test(codex): align grok-4.5 reasoning tier expectations with 4-tier presets
c08040e92 declared xhigh for the grok-4.5 xAI presets but missed the
test expectations, leaving main's frontend checks red and dragging
every PR's Frontend Checks down with the same two failures.
2026-08-31 18:02:21 +08:00
zmq1121
b45b2bd1c9 feat(presets): add Tencent Token Plan presets across six apps (#7011)
* feat(presets): add Tencent Token Plan presets across six apps

Tencent Token Plan is the subscription line (product 1300/1823), separate
from the TokenHub pay-as-you-go marketplace already covered by the Tencent
Hunyuan preset: keys are scoped per site and the lines do not interoperate,
so domestic and intl endpoints are never mixed as candidates. Six products
land across six apps (Claude, Claude Desktop, Codex, OpenCode, Hermes,
OpenClaw):

- personal / enterprise pro / enterprise lite, each in domestic and
  international (Singapore) variants, named with an "(Intl)" suffix —
  clearer than the existing "en" suffix convention since these differ by
  site rather than by language
- endpoints per the plan docs: api.lkeap.cloud.tencent.com/plan
  (domestic personal), tokenhub.tencentmaas.com/plan (domestic
  enterprise), tokenhub-intl.tencentcloudmaas.com/plan (intl)
- the Auto routing id is asymmetric: the domestic personal plan calls it
  tc-code-latest, every other plan calls it auto (verified live — each
  endpoint rejects the other's id)

Lineups, thinking switches and endpoints were verified with real API keys
against all four endpoints (2026-08-31, 124 live calls):

- cross-site calls all fail 401/403, confirming per-site key isolation
- thinking + reasoning_effort both take effect on /plan endpoints, so all
  six Codex presets declare codexChatReasoning with per-model
  reasoningLevels matching observed behavior: glm-5.3 always thinks and
  accepts a strict low/high/max enum (medium/xhigh → 400),
  kimi-k2.7-code(-highspeed) only accepts enabled, minimax-m2.5/m2.7 and
  the domestic auto silently ignore thinking-off, the intl auto honors it
- /plan/v3/models exists only on the domestic personal endpoint, so the
  modelsUrl override is set on that Claude preset alone
- minimax-m2.5 is gone from the plan doc tables but still listed by
  /plan/v3/models and callable under the plans that cover it → included
  there; kimi-k2.5 retired on 8/31 → excluded

OpenCode/Hermes/OpenClaw presets ride the tool-agnostic /plan/v3
OpenAI-compatible endpoint (no subscription-line integration pages exist;
the 1300/80642-80644 guides cover the marketplace line). OpenClaw entries
carry contextWindow from the platform model list and omit cost —
subscription plans have no per-token price. Reasoning levels are
registered in the codexReasoningLevelPresets whitelist (11 rows); the new
tokenPlanProviderPresets test locks endpoints, lineups, key URLs and the
OpenClaw key rebase across all six apps (43 tests).

* fix(presets): address review — OpenClaw official pages, Auto window, glm-5.3 default, region candidates

P1: OpenClaw entries now copy the five fields verbatim from the official
subscription-line integration pages (personal 1823/130062, enterprise
1300/81503) — all-zero cost, official contextWindow/maxTokens (Auto
196608/32768, glm-5 202752/16384, deepseek 1000000/384000 personal /
1048576/393216 enterprise, hy3-preview 262144/131072 reasoning:true),
replacing the previous platform-list derivations; maxTokens was the
consequential omission — OpenClaw defaults it to 8192, truncating long
completions. Models beyond those pages (minimax-m2.7, glm-5.1, glm-5.2,
hy3, kimi-k2.6, glm-5.3, glm-5-turbo, deepseek GA ids) get maxTokens
from the platform model list (1300/78934) with the boundary noted
in comments.

P2: the six Auto routing rows in the Codex catalogs now declare
contextWindow 196608 (the only official figure, from the OpenClaw
pages) — undeclared, the backend falls back to the 128K default and
every preset's default model ships with half its window.

P2: glm-5.3 rows declare defaultReasoningLevel "high" — its strict
low/high/max enum drops the template's medium default, which resolved
to canonical.last() = max, the slowest and most quota-hungry setting
on a fixed-budget plan.

P2: enterprise presets carry a second endpointCandidates entry for
their plan's other region — domestic Guangzhou primary + Singapore
(tokenhub-intl.tencentmaas.com), intl Singapore primary + Guangzhou
(tokenhub.tencentcloudmaas.com) — since both region tables state
cross-region calls are not supported, a customer on the other region
otherwise has no usable preset. Personal plans are single-region and
stay single-candidate.

Also widens the cross-site isolation test to distinguish site domains
(tencentmaas vs tencentcloudmaas) from the tokenhub-intl prefix, since
the same-site Singapore candidate now matches that prefix.

---------

Co-authored-by: zmq1121 <183985311+zmq1121@users.noreply.github.com>
2026-08-31 17:51:16 +08:00
Jason
054673e0b8 fix(codex): reject 2^64 in whole-float integer rewrite
u64::MAX as f64 rounds up to 2^64, so the > guard let exactly 2^64
through; the saturating cast then produced u64::MAX, which rounds back
to 2^64 and passes the round-trip check - silently rewriting the value
off by one. Guard with >= instead.
2026-08-31 09:55:35 +08:00
Jason
d05a11cc21 refactor(codex): drop unused namespace SSE restore helpers
create_namespace_restore_sse_stream and restore_sse_block lost their
last caller when the xAI response path moved to the sanitize rewrite
stream; delete them instead of shipping #[allow(dead_code)].
2026-08-31 09:55:35 +08:00
Jason
9e1100534b fix(codex): address xAI native Responses review findings
- Remap unknown request models before sanitizing, so a subagent SKU
  that lands on grok-4.5 gets its unsupported sampling fields stripped.
- Intersect union-branch required lists instead of unioning them, so a
  flattened oneOf does not force fields the chosen branch lacks.
- Let grok-prefixed request models bypass the catalog remap: real Grok
  SKUs the catalog has not caught up with must pass through.
2026-08-31 09:55:35 +08:00
Loocor
527b56f8b2 fix(ci): clear clippy dead_code and question_mark
CI fails -D warnings on unused namespace SSE helpers and a
let-else that clippy wants as ?.
2026-08-31 09:55:35 +08:00
Loocor
bf325b256f refactor(codex): thin xAI native request gate
Keep Grok sanitizers, agent_message rewrite, and unknown-model
remap behind one forwarder call so upstream rebases conflict
at a single gate instead of three inlined rewrites.
2026-08-31 09:55:35 +08:00
Loocor
dfc9b066dd fix(codex): remap grok unknown models for subagents
xAI 404s Codex role SKUs such as gpt-5.6-sol. Rewrite those
request models onto the provider's configured model, keep
catalog slugs like grok-4.5, and walk the full body for
agent_message items so collaboration 422s log at info.
2026-08-31 09:55:35 +08:00
Loocor
0a9a43785c refactor(codex): simplify xAI agent_message rewrite
Keep the same conversion, but build the message with json!,
share one nonempty-text path for content parts, and drop the
one-shot type helper.
2026-08-31 09:55:35 +08:00
Loocor
914c8bb5af fix(codex): rewrite Grok agent_message input items
Codex multi-agent v2 injects agent_message mailbox items that
xAI Responses cannot deserialize, so child spawns 422 before
any tool runs. Rewrite those items to ordinary user messages
on the native Responses path, flattening encrypted_content
parts to input_text and leaving field-deletion sanitizers
untouched.
2026-08-31 09:55:35 +08:00
Loocor
b7da894b3d fix(codex): sanitize xAI native Responses for Grok
xAI still rejects Codex Desktop tool schemas before sampling, and
Grok still emits whole-float JSON numbers that Codex serde refuses
as i32/u64. Collapse root oneOf/anyOf function parameters on the
request path (#6815) and rewrite integral floats on completed
function_call arguments.

Keep both transforms in transform_codex_responses_xai_sanitize.rs
with a thin native-Responses gate so the patch can be rebased or
cherry-picked until upstream covers the same cases.
2026-08-31 09:55:35 +08:00
Jason
c08040e926 fix(codex): declare xhigh reasoning tier for grok-4.5 xAI presets
Probed 2026-08-30 against native /v1/responses: grok-4.5 accepts
low/medium/high/xhigh and rejects max with HTTP 400 "Invalid reasoning
effort". Codex does not clamp out-of-catalog efforts, so the preset
list must match what the upstream actually accepts.
2026-08-30 23:53:14 +08:00
Jason
c88b00faf6 fix(codex): neutralize managed-OAuth official-auth fallback flag
The v3.20.1 config-only switch refactor extended the keyless safety gate
to all write paths, which broke switching to proxy-managed OAuth cards
(xAI Grok OAuth): they are keyless by design — the local proxy injects
the real token per request — yet their preset snapshot inherited the
pre-0.149 template's requires_openai_auth = true, so the gate refused
the switch with provider.codex.config.official_auth_fallback.

Fix the snapshot instead of exempting the gate: force the flag to false
on the active custom table for proxy-injected OAuth providers
(uses_proxy_injected_oauth: xai_oauth, github_copilot — codex_oauth is
deliberately excluded, the official login IS its credential). The
neutralization hooks into build_effective_provider_for_live so
preflight, normal writes, and proxy backup/takeover projections all see
the same safe shape, and 0.149 resolves the provider as unauthenticated
without ever reading auth.json. The OAuth preset now emits
requires_openai_auth = false at the source; stored cards self-heal via
backfill on the next switch.
2026-08-30 23:27:03 +08:00
htyvista
d8065cc628 fix(proxy): preserve mid-conversation system messages for prefix cache (#6941)
* fix(proxy): preserve mid-conversation system messages for prefix cache

Remove normalize_openai_system_messages which merged all system messages
to the head, breaking radix prefix cache on every turn when Claude Code
injects <total_tokens> metadata as a mid-conversation system message.

Now system messages from the top-level  field are placed first,
and all messages (including mid-conversation system) are extended in
their original order without any merging or reordering.

Also add custom-protocol feature for standalone release builds.

* fix(proxy): restore top-level system merge, add regression test

Restore merging of top-level system array blocks into a single system
message (cross-turn stable, does not affect prefix cache). Mid-conversation
system messages from the messages array remain in place without merging
or reordering.

Add regression test to lock the new behavior: mid-conversation system
messages stay at their original position.
2026-08-29 22:55:03 +08:00
Jason
3217f72596 docs(release): add v3.20.1 release notes v3.20.1 2026-08-28 08:19:45 +08:00
Jason
9485cf2f3d chore(release): v3.20.1 2026-08-28 08:19:45 +08:00
Jason
5ff199b5a9 fix(usage): align session sync card spacing and gate Sync Now to manual mode
The session sync card sat in the page-level space-y-8 stack while the
pricing/maintenance cards below use space-y-4 inside the Accordion,
leaving an inconsistent 32px vs 16px gap. Group the sync card with the
Accordion under a shared space-y-4 wrapper so all three cards align.

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

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

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

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

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

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

Also wires up the previously unused sync_session_usage command as the
manual sync entry point, and adds usage.sessionSync.* strings in all
four locales.
2026-08-27 23:35:02 +08:00
SaladDay
6243e20ad6 fix(codex-oauth): align identity tests with JWT parsing (#6831)
Remove the test-only identity fallback from Codex id-token extraction, require a valid compact JWT shape (three segments, base64url JSON header with a non-empty alg) before trusting the subject claim, and use structurally valid JWT fixtures throughout managed-account identity flows.
2026-08-27 15:30:59 +08:00
Jason
270a4ff372 feat(usage): add OpenCode Go subscription usage query via token plan path
Query the official first-party (undocumented) endpoint
GET https://opencode.ai/zen/go/v1/usage — Bearer-only auth, the opposite
of the inference-side /messages which only accepts x-api-key. The 200
response shape was verified against a real Go subscription key.

- Map rolling/weekly/monthly windows onto the existing five_hour /
  weekly_limit / monthly tiers: no new i18n keys or tray label groups
- Parse each window defensively (upstream reshaped the response within
  an hour of launching it); report an explicit error when no window
  parses instead of rendering an empty card
- Drop resetsAt when percent is 0: the upstream value is a now+window
  placeholder, not a real reset time
- Treat 403 as "valid key but no Go subscription" (Zen and Go share
  the same workspace key), distinct from 401 auth failures
- Auto-enable the usage script on newly added providers across all five
  apps shipping the OpenCode Go preset; claude keeps its existing
  all-coding-plans behavior while claude-desktop/codex/opencode/pi only
  opt in for OpenCode Go
2026-08-27 08:37:56 +08:00
Jason
c5e4f70545 fix(codex): stamp requires_openai_auth to match login preservation on third-party switches
Presets and the custom template carried requires_openai_auth = true from
the pre-0.149 era when auth.json held the third-party key. On 0.149 the
injected bearer decides request auth either way, but the stale flag traps
the TUI in the login screen once preservation-off deletes auth.json, while
a preserved login stays hidden and unrefreshed without it.

The third-party write plan now overrides the stored flag with the
preservation setting (off -> false, on -> true), but only on the active
custom table that short-circuits request auth via env_key or the injected
experimental_bearer_token - stamping true on a keyless header-auth table
would route the preserved OAuth login to the third-party endpoint. The
takeover subsystem keeps its existing shapes. The preservation setting is
now an explicit plan parameter so plan output is deterministic in tests.
2026-08-27 08:37:56 +08:00
Jason
58687bd6bd docs(codex): reunite the legacy-reroute doc comment with its function
Splitting first_free_cc_switch_provider_id out of the normalizer left the
normalizer's doc comment stranded on the helper, with one sentence
truncated mid-clause. Move it back above
normalize_codex_legacy_openai_reroute; the helper keeps only its own
description. Comment-only change.
2026-08-27 08:37:56 +08:00
Jason
877df74f98 fix(codex): backfill names on official writes, preflight table conflicts 0.149 rejects
The official branch of plan_codex_live_write never goes through
prepare_codex_provider_live_config, so idle name-less custom tables left
by older versions survived official writes and made Codex 0.149 reject
the whole config — the branch now backfills names itself.

Add an independent semantic preflight over every provider table (idle
ones included, official and third-party alike): aws outside the Bedrock
built-ins and auth combined with requires_openai_auth / env_key /
experimental_bearer_token are combinations 0.149 refuses to load and
cannot be normalized away, so the switch now fails up front with an
actionable error instead of writing a config Codex won't start on. The
auth-safety gates only judge the active route and are skipped when a key
is carried, so they never covered these shapes. Gate-less paths (proxy
backup/restore) intentionally stay untouched.
2026-08-27 08:37:56 +08:00
Jason
798602c359 fix(codex): backfill names for all custom tables, never for Bedrock ones
Codex 0.149 rejects the whole config when any non-Bedrock provider table
has an empty/missing name, active or not — plain config-only switches
never went through the update path's backfill, so prepare now normalizes
every custom table (keyless and official writes included).

The Bedrock built-ins are the mirror image: they only allow
base_url/auth/http_headers/aws.* overrides, and writing name there makes
the built-in merge reject the whole config — the proxy takeover rewrites
base_url through update_codex_toml_field, so its backfill now skips both
reserved ids.

Also drop auth/aws as own-credential short-circuits from the official-auth
fallback predicate: 0.149 validates both as mutually exclusive with
requires_openai_auth, so tables combining them are dead configs the whole
file fails to load with — flagging them keeps them from being written.
2026-08-27 08:37:56 +08:00
Jason
93bb91aad3 fix(codex): make migrated tables loadable, follow non-fallback routes, unreserve oss/ollama-chat
- Normalize migrated stale reserved tables into a shape Codex 0.149
  loads: wire_api forced to "responses" (the chat wire API was removed
  upstream; any other value fails deserialization of the whole config)
  and empty/missing name backfilled. update_codex_toml_field backfills
  name too - it historically created name-less tables that 0.149
  rejects at load.
- Key the migration route-follow on the 0.149 auth.json fallback
  boundary (requires_openai_auth=true without env_key / bearer token /
  auth / aws) instead of "table carries credentials": scoped-token,
  header/query-param auth and unauthenticated local tables keep their
  legitimate third-party route; only credential-less
  requires_openai_auth tables still snap back to the built-in provider.
  The safety-gate predicate shares the same table-level check and now
  honors an in-table experimental_bearer_token short-circuit.
- Remove oss/ollama-chat from both reserved-id lists (Rust + TS
  mirror): neither is reserved on Codex 0.148/0.149, both load as
  ordinary custom tables, so the bearer token must land inside them
  instead of a dead top-level field.
- Match model_provider case-sensitively in official-identity detection
  (proxy + frontend), mirroring upstream: "OpenAI" routes to a custom
  table, not the official provider.
2026-08-27 08:37:56 +08:00
Jason
bb54e87a78 fix(codex): credential-aware routing for migrated tables, exact-match shared predicate, migrate all reserved ids
Fifth hardening round on the config-only unification:

- The stale-table migrator now covers all three reserved ids Codex
  refuses at load (openai, ollama, lmstudio — exact lowercase, bedrock is
  exempt upstream) and routes credential-aware: when the renamed table
  was the active route it is followed only on a third-party write that
  can authenticate against it (an injectable key exists, or the table
  carries its own env_key / auth / aws / plain Authorization headers).
  The previous round always snapped back to the built-in provider, which
  silently billed the preserved official account for third-party cards
  that DID have a key. Credential-less tables still snap back so a
  requires_openai_auth fallback can never resolve auth.json against a
  stale address; official writes never follow.
- is_custom_codex_model_provider_id (Rust and its TS mirror) now matches
  reserved ids exactly, aligning with upstream's case-sensitive built-in
  lookup: [model_providers.OpenAI] is a legitimate custom provider whose
  table receives the bearer token, instead of the token dying in a
  top-level field while the safety gates skipped the custom table.
- The migrator runs before the safety gates in the switch plan so the
  gates judge the text that will actually be written.
- The auth-cleanup warning no longer hardcodes ~/.codex/auth.json (wrong
  on Windows and with a custom Codex dir); all four locales updated.
2026-08-27 08:37:56 +08:00
Jason
43818101e8 fix(codex): rename stale reserved tables losslessly, match reserved ids exactly, cover inline tables
Fourth hardening round on the config-only unification:

- Replace the lift-base_url strip with a lossless rename: a stale
  [model_providers.openai] table (rejected at load since Codex 0.148) is
  renamed to the first free cc-switch id keeping every user key, with
  wire_api = "responses" defaulted in. The active route deliberately
  snaps back to the built-in provider and no base_url is promoted
  anywhere — the previous strip lifted the stale address into a live
  top-level reroute AFTER the safety gates had run (and on the official
  branch, which has no gates), sending whatever auth.json holds to an
  unvalidated stale address. The shape never loaded since 0.148, so there
  is no working behavior to preserve.
- Match reserved ids exactly, mirroring upstream's case-sensitive
  validate_reserved_model_provider_ids: `OpenAI` and other case variants
  are legitimate custom ids and are no longer touched by the migration,
  the reroute normalization, or update_codex_toml_field.
- Handle inline `model_providers = { … }` containers in the legacy
  reroute normalization (insert an inline member matching the container
  style) — proxy backup/restore call prepare without the safety gates,
  so skipping inline shapes left the key in a dead top-level field next
  to live auth.json credentials.
- Refuse update_codex_toml_field for the built-in ollama/lmstudio ids:
  they have no top-level reroute knob and any table for them makes Codex
  refuse the whole config, so a takeover of such a card now fails with
  an actionable error instead of writing a config Codex cannot start
  with. Bedrock ids (exempt upstream) and case variants keep the normal
  custom-table path.
- Route switch warnings by code in the frontend: codex_auth_cleanup_failed
  now shows its own localized message (zh/en/ja/zh-TW) instead of the
  unrelated "backfill failed" text.
2026-08-27 08:37:56 +08:00
Jason
9a1a6b83ab fix(codex): migrate stale reserved openai tables, suffix conflicting migration ids, surface auth cleanup failures
Three follow-up hardenings on the config-only unification:

- Strip a stale [model_providers.openai] table on every write path
  (official included) and in prepare_codex_provider_live_config: older
  cc-switch takeover projections created that table, and Codex 0.149
  rejects the whole config at load when a reserved built-in id is
  overridden — the switch reported success while Codex could no longer
  start. The table's base_url (the only key that ever had an effect) is
  lifted to the top-level openai_base_url knob when the built-in provider
  is the active route, feeding the existing reroute normalization.
- When a user-authored [model_providers.cc-switch] table already exists,
  the legacy-reroute migration now continues under the first free
  suffixed id (cc-switch-2, …) instead of giving up: refusing left proxy
  backup/restore — which call prepare without the safety gates — writing
  an unmigrated reroute with live auth.json credentials next to it.
- With preservation off, a failed auth.json deletion after a third-party
  switch now surfaces as a SwitchResult warning
  (codex_auth_cleanup_failed) instead of only a log line, so the user can
  see the official login is still on disk. The switch itself still
  succeeds — config and current are already committed.
2026-08-27 08:37:56 +08:00
Jason
97a7425f7f fix(codex): preflight switches before committing current, share normalization with proxy paths
Four hardening fixes on top of the config-only unification:

- Split write_codex_live_for_provider into a pure plan builder
  (normalization, safety gates, token injection, TOML parsing) and an
  executor, and preflight the plan in switch_normal BEFORE current is
  committed. Previously a write-layer refusal landed after local/DB
  current had moved, so the next switch would backfill the old live
  config into the refused provider's DB row.
- Sink the legacy openai_base_url normalization into
  prepare_codex_provider_live_config, making it the single
  normalize→inject entry point. Takeover backup rebuilds and restore call
  prepare directly; without the migration a restored legacy backup left
  the key in a top-level field Codex ignores while auth.json credentials
  stayed live.
- Stop update_codex_toml_field from creating [model_providers.openai]:
  Codex 0.149 rejects the whole config when a reserved built-in id is
  overridden (validate_reserved_model_provider_ids). The built-in openai
  provider is rerouted via the top-level openai_base_url knob instead,
  and wire_api writes become a no-op for it.
- Never overwrite a user-authored [model_providers.cc-switch] table
  during normalization — nothing proves it is ours, and their headers or
  query params would be lost and backfilled into the DB. The shape falls
  through to the safety gates instead.

The keyless-fallback rejection test now also locks that a refused switch
leaves current on the previous provider.
2026-08-27 08:37:56 +08:00
Jason
cbb7912776 refactor(codex): unify provider switching on config-only auth writes
Since Codex 0.149 (openai/codex#39214) custom providers no longer inherit
ambient auth from auth.json, so the API key must travel as a
provider-scoped experimental_bearer_token in config.toml (honored since
Codex 0.48; verified against rust-v0.148 sources). Instead of dual-writing
auth.json + config.toml depending on the preservation setting, make every
third-party switch config-only:

- auth.json is reserved for the official ChatGPT login: preserved when
  the preservation setting is on, deleted otherwise. It never carries
  third-party keys, so official OAuth cannot leak to a third-party
  endpoint through a requires_openai_auth fallback or a lingering login.
- The legacy reroute shape (built-in openai provider + top-level
  openai_base_url) is normalized into a cc-switch-owned custom provider
  table pinning wire_api = "responses", giving the key a provider-scoped
  slot instead of a top-level field Codex ignores.
- Two safety gates guard every third-party write (previously only the
  preservation path): a carried key with no provider table to land in is
  refused, and a keyless config that would resolve auth from auth.json
  (requires_openai_auth without provider-own credentials, or an
  openai_base_url reroute) is refused.
- Token injection skips provider tables carrying their own credentials
  (env_key / auth / aws / Authorization headers); requires_openai_auth
  overrides the header guard so the injected token short-circuits the
  auth.json fallback. Inline provider tables are handled in
  set/extract/remove, and amazon-bedrock-runtime joins the reserved
  provider id lists (Rust + TS).

Official cards are unchanged. Takeover placeholder writes ride in
config.toml the same way, which is what Codex >= 0.149 actually sends to
the local proxy.

Known trade-offs: pre-0.48 Codex releases (which never read the token
field) lose third-party auth, and an empty third-party config with a key
is now refused on every path instead of riding in auth.json.

Refs #6746, #6744
2026-08-27 08:37:56 +08:00
Thefool
926af94923 fix(provider): always project edits to live configuration (#6779)
Centralize live ownership decisions for provider updates and resyncs, refresh stale backups without diverting writes, and re-project active universal-provider children to their live files. Report per-application projection failures instead of returning a false success. Require takeover evidence in addition to a persisted enabled flag, including per-app activation locks, and add regression coverage for ordinary, stale-backup, interrupted-teardown, and universal-provider paths.

Co-authored-by: BingZi-233 <lhby233@outlook.com>
2026-08-26 23:51:33 +08:00
ikim
0ae561b8d2 fix(ci): run WSL2 contract tests via prebuilt binaries (#6472)
* ci: run WSL2 contract tests via prebuilt binaries

link.exe/mt.exe fails to create manifests when TEMP points to a WSL UNC path (LNK1327). Run precompiled test binaries directly instead of re-invoking cargo, and explicitly set native TEMP during compilation.

* ci: harden prebuilt WSL2 contract runner

Keep the scheduled full-suite workflow unchanged until it can be validated independently. Drain Cargo's JSON stream before using the test binary and propagate its exit status.

---------

Co-authored-by: saladday <1203511142@qq.com>
2026-08-26 22:55:12 +08:00
Thefool
bbe8bb93ab fix(codex): reconcile edit form key with live bearer token (#6534)
* fix(codex): keep provider's own key on switch-away backfill

When switching away from a Codex provider, cc-switch backfills the
outgoing provider's stored config from the live ~/.codex files so that
in-app changes are captured. restore_codex_provider_token_for_backfill
decides what to persist as the provider's auth.OPENAI_API_KEY:

- When the live config.toml carries a per-provider
  experimental_bearer_token, that token is provider-scoped and is lifted
  back into the stored auth. Correct.

- When it does NOT (the default preserve_codex_official_auth_on_switch
  = false mode keeps the active key in the shared auth.json), the
  function returned early and the caller adopted the live auth.json
  wholesale as the provider's stored auth.

The problem: auth.json is a single-slot shared file with no provider
identity; it always holds the most-recently-activated provider's key.
Any time it holds ANOTHER provider's key (after a proxy-takeover
backup/restore cycle, an in-app ChatGPT login overwriting auth.json,
a cloud-sync current/live divergence, or any current-vs-live mismatch),
the switch-away backfill overwrote the outgoing provider's stored key
with another's. Repeated switches made keys silently converge across
providers that share a base URL, surfacing as "model xxx not found"
because the wrong key reached a model it was not entitled to.

Fix: in the no-bearer branch, preserve the provider's own DB-stored
auth instead of adopting the shared live auth.json. Live config.toml
changes (model/base_url/mcp/env) are still captured by the caller; only
the credential slot is no longer taken from the shared file. This
mirrors the #6277 restore-side rule of never letting the shared live
credential clobber per-provider storage.

Adds two regression tests:
- backfill_keeps_provider_own_key_when_live_auth_holds_another_key
  (the failing case under the old code)
- backfill_restores_key_from_live_bearer_token_when_present
  (positive control for the already-correct bearer path)

Fixes #6414.

* Revert "fix(codex): keep provider's own key on switch-away backfill"

This reverts commit 4ddf304eca.

* fix(codex): reconcile edit form key with live bearer token

In bearer-token mode (preserveCodexOfficialAuthOnSwitch enabled — used to
keep a ChatGPT login while routing third-party providers), switching to a
provider writes its key to config.toml's experimental_bearer_token and
preserves the shared ~/.codex/auth.json. auth.json is a single shared slot
with no provider identity, so it may hold ANOTHER provider's stale key
(left over from a previous default-mode switch or a different provider).

When the user then opened the edit dialog for the current provider,
EditProviderDialog loaded liveSettings as the form base, and
useCodexConfigState initialized codexAuth from the stale auth.json while
pickCodexApiKey preferred auth.OPENAI_API_KEY over the bearer token. The
form therefore displayed — and on save persisted — the stale key back
into the provider's DB record. Repeated edits made keys silently converge
across providers that share a base URL, surfacing as "model xxx not found"
once the wrong key reached a model it was not entitled to (#6414).

Fix: when loading Codex config into the form, lift the config's
experimental_bearer_token into auth.OPENAI_API_KEY (when present and
differing) so both the displayed key and the saved auth carry the
correct per-provider key. This mirrors the backend
restore_codex_provider_token_for_backfill lift and is a no-op when the
config has no bearer (default mode keeps auth.json as the active key
slot, and manual live auth edits are preserved exactly — covered by
existing tests).

Adds 4 regression tests in tests/hooks/useCodexConfigState.bearer.test.ts.

Fixes #6414.

* style: prettier-format useCodexConfigState.ts

Fixes the frontend CI "Check formatting" failure on the previous commit.
Pure formatting (collapses a multi-line call to one line); no behavior
change.

* fix(codex): scope bearer reconciliation to live edits
2026-08-26 17:21:30 +08:00
yovinchen
bd15ea1193 feat(terminal): add Otty support with tab-aware launching (#6620) 2026-08-26 15:55:13 +08:00
SaladDay
c2ec78dd47 fix(codex-oauth): isolate shared workspace accounts (#6780)
* fix(codex-oauth): isolate shared workspace accounts

* fix(codex-oauth): preserve identity lifecycle invariants
2026-08-26 15:18:18 +08:00
Sailing Loong
c911c7e3c6 fix(prompts): keep unmanaged prompt files intact when a restore enables none (#6810)
A WebDAV/S3 download (or backup import) restores db.sql and then projects
the imported prompt rows back to live files. When the restored database
had no enabled prompt for an app, project_prompt_set_to_path overwrote
the local prompt file (e.g. ~/.config/opencode/AGENTS.md) with an empty
string — destroying local content that was never part of the sync
payload, since these files are not uploaded.

Drop the clear-on-nothing-enabled branch: this projection is only
reachable from restore paths, where "no enabled prompt in the snapshot"
says nothing about the local file. Disabling the last prompt from the UI
still clears the file through PromptService::upsert_prompt.

Fixes #6778

Co-authored-by: SailingLoong <309827397+SailingLoong@users.noreply.github.com>
2026-08-26 11:55:03 +08:00
Jason
9a596158ca chore(presets): move TeamoRouter to teamorouter.cn, keep .com as fallback
Switch the TeamoRouter domain and API base URL to teamorouter.cn across all
eight managed apps (Claude Code, Claude Desktop, Codex, Grok Build, OpenCode,
OpenClaw, Hermes, Pi), and update the sponsor links in all four READMEs.

The previous api.teamorouter.com addresses are kept as fallback endpoints in
the four presets whose interface supports endpointCandidates (claude,
claudeDesktop, codex, grokBuild), each matching its file's path tier - bare
host for the Anthropic-native presets, /v1 for Codex and Grok Build.
2026-08-24 16:49:56 +08:00