Commit Graph

902 Commits

Author SHA1 Message Date
Jason 16f988bd1a feat(providers): auto-sync Claude common config from live on switch
When switching away from a Claude provider that opted into common config, re-extract the shareable portion of its live settings.json and replace the stored snippet. This captures config the user added directly in the app (plugins/hooks/shared prefs) so it isn't lost on switch, and propagates deletions so a removed key isn't re-injected on the next switch.

Scoped to Claude providers with common_config_enabled, skipped when the snippet was explicitly cleared. All failures are non-fatal (warn only) and never block the switch.

Also harden extract_claude_common_config to strip ALL credential-like keys via pattern match (*_API_KEY / *_AUTH_TOKEN / secret / token / etc.), not just ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN. Claude providers legitimately use OPENROUTER_API_KEY / GOOGLE_API_KEY (and may carry OpenAI/Gemini/AWS Bedrock/Vertex creds), which previously could leak into the shared snippet and be injected into other providers. This also fixes the pre-existing leak in the manual Extract and one-time auto-extract paths. Plural _TOKENS (e.g. MAX_OUTPUT_TOKENS) is preserved as shareable.

Tests: 4 integration (capture / delete-sync / opt-out / cleared) + 1 unit (credential stripping).
2026-06-29 23:30:58 +08:00
Jason 7f83feb222 fix(presets): use dated Doubao model id and price 6-digit dated models
Volcengine Ark rejects the bare model name doubao-seed-2-1-pro with a 404
("model does not exist or you do not have access to it") even after the
model is activated; the API requires the full dated id
doubao-seed-2-1-pro-260628. Update the Doubao preset model id across all
apps (config default, generated catalog, and OpenClaw namespaced refs).

Pricing lookup only stripped 8-digit (YYYYMMDD) and ISO date suffixes, not
the 6-digit YYMMDD format Volcengine uses (-260628, -250615), so real
Doubao usage never matched the bare-name pricing seed and showed $0 cost.
Extend strip_model_date_suffix to also strip 6-digit YYMMDD (with
month/day validation to avoid eating non-date version suffixes), keeping
the bare-name pricing seed as the canonical identity. This also fixes
pricing for every other Volcengine Doubao model. Add unit and end-to-end
regression tests.
2026-06-29 23:30:58 +08:00
Jason 26f0d221c0 fix(codex): reject web_search for Qwen3-Coder and correct LongCat context window
- Add `qwen3-coder` to the web_search reject model-prefix blacklist so the
  native qwen3-coder-plus direct-connect preset suppresses the built-in tool
  (百炼 rejects it for the coder series), while general Qwen models sharing the
  DashScope host keep web_search enabled. Matched on the model axis, not host.
- Correct LongCat-2.0-Preview context window from 128K to its real 1M
  (1048576), aligning with the MiMo/Qwen 2^20 convention.
- Tighten native Responses preset tests to assert exact model→contextWindow
  catalogs instead of only checking catalog presence.
2026-06-29 23:30:58 +08:00
Jason d380b410af fix(codex): disable web_search for native gateways that reject it
Some native /responses gateways whose first-party models lack OpenAI's
hosted web_search tool (MiMo, LongCat, MiniMax) reject a web_search tool
with "tool type 'web_search' is not supported by this gateway phase".
Codex sends the tool by default (config-driven, not gated by the catalog's
supports_search_tool), so cc-switch now writes the top-level
`web_search = "disabled"` line per those vendors' official Codex docs.

Scope is a blacklist (default-on): only providers matched by base_url host
or model brand prefix are disabled; relays serving real GPT, DouBao, Qwen,
and any unknown provider keep Codex's default. Matching by model brand (not
just host) also catches aggregators (e.g. SiliconFlow) fronting a reject
vendor's model. The field is injected alongside model_catalog_json at
switch time and removed via an ownership sentinel, so existing providers
need no re-save and switching back re-enables web search.
2026-06-29 23:30:58 +08:00
Jason 15e712e779 feat(codex): support native Responses direct-connect with generated model catalog
Codex providers can now run in two modes per provider:

- Proxy-Chat (route takeover): apiFormat=openai_chat, existing Responses<->Chat conversion. Unchanged.

- Native-Responses (direct): apiFormat=openai_responses, no proxy. cc-switch generates ~/.codex/cc-switch-model-catalog.json so Codex shows the custom models and tools work without the freeform apply_patch (type=custom) tool that native gateways like MiMo reject; editing falls back to shell_command.

Catalog generation is keyed on apiFormat (CodexCatalogToolProfile), decoupled from the takeover toggle, so native providers persist a catalog without enabling local route mapping.

Codex's catalog parser requires base_instructions on every entry; the native template carries a neutral default and per-vendor official text overrides it (MiMo, MiniMax). Synthesized catalogs for Qwen/Doubao/LongCat use the neutral default.

Existing native providers must be re-saved once to regenerate a valid catalog (no DB migration).
2026-06-29 23:30:58 +08:00
thisTom 61d7ac01fb fix(hermes): Windows 上 Hermes 供应商配置不生效 (#4680)
* fix(hermes): resolve config dir via HERMES_HOME and platform defaults

On Windows, CC Switch hardcoded `~/.hermes` as the Hermes config directory,
but Hermes resolves it through `get_hermes_home()`: `HERMES_HOME` env var,
then a platform default (Windows `%LOCALAPPDATA%\hermes`, mac/Linux
`~/.hermes`). So CC Switch wrote configs where Hermes never reads them and
provider changes had no effect (refs #3178).

`get_hermes_dir()` now mirrors Hermes' own resolution order:
  1. `settings.hermes_config_dir` — CC Switch explicit override
  2. `HERMES_HOME` env var — trimmed, non-empty, taken as-is (no `~`
     expansion, matching Hermes' `Path(val)`)
  3. platform default — Windows reads the `LOCALAPPDATA` env var (the exact
     value Hermes reads, not a re-derived Known-Folder path) and falls back
     to `~\AppData\Local\hermes`; mac/Linux keep `~/.hermes`

Relation to #3470: that PR dropped `HERMES_HOME` for "consistency with
Codex/Claude". But unlike those tools Hermes treats `HERMES_HOME` as a
first-class mechanism (its own Windows installer sets it), so ignoring it
reintroduces the same mismatch for relocated installs. This keeps it.

Tests cover HERMES_HOME precedence, settings-override > HERMES_HOME, blank
HERMES_HOME fall-through, the platform-correct default, and a pure
`windows_local_hermes_dir` helper exercising the LOCALAPPDATA-set and
LOCALAPPDATA-empty fallback paths on every host.

* test(hermes): isolate config-dir tests from ambient HERMES_HOME/LOCALAPPDATA

`get_hermes_dir()` now consults `HERMES_HOME` (all platforms) and `LOCALAPPDATA`
(Windows). The `with_test_home` helper only neutralized `CC_SWITCH_TEST_HOME`,
so an ambient `HERMES_HOME` — which Hermes' own Windows installer sets — would
make tests that write `get_hermes_config_path()` escape the temp home and touch
a real Hermes config (and the same hazard exists via `LOCALAPPDATA` on Windows).

Clear and restore both env vars in `with_test_home`. This is safe: only this
module reads those env vars, and `dirs` uses the Known-Folder API rather than the
env var. Adds a test asserting both are neutralized inside `with_test_home`.

Addresses the Codex review comment on #4680.

* fix(hermes): trim LOCALAPPDATA to match Hermes' .strip()

`windows_local_hermes_dir` checked `OsStr::is_empty()` on the raw value, but
Hermes does `os.environ.get("LOCALAPPDATA", "").strip()`. A whitespace/padded
`LOCALAPPDATA` would make CC Switch write under the literal padded path while
Hermes trims and falls back, hiding the config from Hermes. Trim before the
empty check, matching the existing `HERMES_HOME` handling. Adds
`windows_local_hermes_dir_trims_localappdata`.

Addresses the Codex review comment on #4680.

---------

Co-authored-by: thisTom <19346741+thisTom@users.noreply.github.com>
2026-06-28 17:49:52 +08:00
yyhhyyyyyy d1f6c74be8 fix(usage): treat usage_script credentials as explicit overrides (#4654)
* fix(usage): treat usage_script credentials as explicit overrides

* fix: treat usage script credentials as explicit overrides
2026-06-28 17:37:58 +08:00
Jason f9547da930 chore(release): bump version to 3.16.4 2026-06-27 10:52:21 +08:00
Jason 2e547c98b1 feat(pricing): add Doubao Seed 2.1 Pro/Turbo model pricing
Volcengine official list price (CNY converted at ~7.14, USD/1M): pro input 0.84 / output 4.2 / cache-hit 0.17; turbo input 0.42 / output 2.1 / cache-hit 0.08. cache_creation kept 0 (Doubao bills cache storage by time, not per-token writes). Appended via INSERT OR IGNORE; 2.0 rows retained for historical usage accounting.
2026-06-26 09:14:10 +08:00
Chenx Dust 1a0e8c7a44 fix: decompress body before forward and support zstd (#3817)
* fix(proxy): decompress Codex request body before forward, support zstd

Codex Desktop sends zstd-compressed request bodies when authenticated
against the Codex backend, which broke local proxy routing because the
handlers parsed the raw bytes with serde_json directly.

Reworked on top of current main so it preserves the response_processor
behavior that landed after this PR was first opened:

- Extract content-encoding helpers into a shared proxy::content_encoding
  module. decompress_body keeps returning Option<Vec<u8>> so unknown
  encodings stay pass-through with their content-encoding header intact,
  and keeps the deflate zlib-then-raw fallback (RFC 9110).
- Add zstd/zst support (zstd 0.13) and disable reqwest's auto zstd
  decompression via .no_zstd() for parity with gzip/br/deflate.
- Decompress the request body before JSON parsing in the three Codex
  handlers (chat_completions / responses / responses_compact) and strip
  the stale content-encoding / content-length / transfer-encoding headers
  so the forwarder regenerates them.
- Support stacked codings (e.g. "gzip, zstd") by decoding in reverse
  order and merge repeated Content-Encoding headers via get_all.

Fixes #3764
Fixes #3696

Co-authored-by: chenx-dust <16610294+chenx-dust@users.noreply.github.com>

* fix(proxy): decompress upstream error bodies before reading them

The forwarder error branch consumes non-2xx responses via String::from_utf8
directly, bypassing read_decoded_body. reqwest has no auto-decompression
feature enabled, so a compressed error body (gzip/br/deflate/zstd) arrives
as raw bytes, fails from_utf8, and gets dropped, hiding upstream rate-limit
and auth details from the client.

Decode the error body with the shared proxy::content_encoding helper,
mirroring the success path. Falls back to the raw bytes when the encoding is
unsupported or decoding fails.

Co-authored-by: chenx-dust <16610294+chenx-dust@users.noreply.github.com>

---------

Co-authored-by: Jason <farion1231@gmail.com>
Co-authored-by: chenx-dust <16610294+chenx-dust@users.noreply.github.com>
2026-06-26 08:50:59 +08:00
zhaoyuan 524b9d9825 fix(proxy): 修复 Copilot/Codex OAuth 模块绕过全局代理导致 Claude 模型 400 错误 (#4583)
* fix(copilot): 修复 GitHub Copilot 出站请求不走全局代理导致 model not supported

* fix(proxy): 修复全局代理客户端配置未被应用到 Copilot/Codex OAuth 模块

两个关键问题:

1. CopilotAuthManager 在构造时写死 Client::new(),使得拉取 /models 列表、
   换取 token 等认证流程无视全局代理配置(global_proxy_url),直连目标服务。
   结果:直连时 /models 返回 0 个 Claude 模型 → live resolution 失效 →
   模型 ID 无法正确归一化/匹配 → Copilot 上游返回 400 model_not_supported。

2. CodexOAuthManager 也有完全相同的问题,导致 Codex OAuth 认证请求绕过代理。

改动:删除两个模块的自持 http_client 字段,改为每次请求时从全局客户端现取
(crate::proxy::http_client::get())。这样:
- 遵循全局代理 URL 配置
- 支持运行时热更新代理设置
- 符合代码库设计意图(http_client.rs 注释明确说"所有 HTTP 请求应使用此模块")

修复覆盖范围:
- copilot_auth.rs: 7 处调用(token 获取、/models 拉取、model vendor 判断)
- codex_oauth_auth.rs: 4 处调用(device code、OAuth token 刷新)

Fixes #2016 #2931
2026-06-25 16:05:23 +08:00
Jianfeng Mei 6fd4e6f462 feat(proxy): 添加本地代理请求覆盖功能,支持自定义请求头和请求体 (#4589)
* feat(proxy): 添加本地代理请求覆盖功能,支持自定义请求头和请求体

* fix(proxy): harden local request overrides validation

* feat(proxy): 添加受保护的本地代理请求头名称验证功能

* fix(i18n): 更新本地代理请求覆盖的错误提示信息格式

---------

Co-authored-by: jason.mei <jason.mei@ucloud.cn>
2026-06-24 22:19:01 +08:00
SaladDay edeee25fae feat(db): in-app recovery screen with upgrade button when DB version is too new (#4575)
* feat(db): in-app recovery screen with upgrade button when DB version is too new

When the SQLite user_version is newer than the app supports (SCHEMA_VERSION),
Database::init() fails and previously dead-ended in a native Retry/Exit dialog
(Retry just fails again). The app now boots a dedicated recovery screen instead.

- Detect the recoverable "version too new" case and surface it via init_status
  (kind="db_version_too_new" + db_version/supported_version); force-show the main
  window and skip normal AppState boot (recovery commands need only AppHandle).
- The recovery screen first checks for an available update:
  - update available  -> "Upgrade app" runs the updater (download + install +
    restart) with a download progress bar.
  - no update (already latest) -> warns that the DB is too new even for the latest
    build (likely a third-party client), so upgrading cannot help.
- install_update_and_restart now emits `update-download-progress` events; new
  `check_app_update_available` command.
- i18n: en / ja / zh / zh-TW.

Verified: pnpm typecheck + format:check + test:unit (351) green; cross-model
review of Rust correctness and recovery-mode safety (no AppState panic).

* fix(db): pre-check too-new DB before schema writes; exit on close in recovery mode

Addresses review feedback on the too-new-DB recovery flow.

- P1: stored_user_version_exceeds_supported() is now checked BEFORE
  Database::init(), so create_tables()'s DDL (incl. the unconditional
  DROP INDEX/DROP TABLE IF EXISTS failover_queue) never runs against a
  database whose user_version we cannot understand. The earlier
  post-init-failure recovery branch is removed; the pre-check is the
  single authoritative guard, so "don't write to a DB we can't read"
  now holds from the very first DB access.

- P2: the window CloseRequested handler now detects recovery mode
  (init_error kind = db_version_too_new) and exits the app instead of
  honoring minimize_to_tray_on_close. Recovery mode returns before the
  tray is created, so hiding the window would leave the app running with
  no tray to bring it back; native-close now quits cleanly.

Verified: cargo fmt --check clean; cross-model Rust review of both fixes
(compile-correctness + no normal-startup/close regression).
2026-06-24 15:07:42 +08:00
MoonDreamStars 2db3163cf2 ci: add Windows ARM64 release support (#3950)
* ci: add Windows ARM64 release artifacts

* ci: keep release matrix jobs independent

* ci: fix pnpm cache path on Windows runners

* ci: setup pnpm with corepack on Windows ARM64

* ci: fix Windows ARM64 release build

* ci: retry transient release bundler downloads

* ci: remove non-minimal release workflow changes

* ci: keep release matrix jobs independent

macOS signing fails in forks without Apple secrets and, with default
matrix fail-fast, cancels the sibling jobs (including Windows ARM64)
before they finish. Disable fail-fast so each platform runs to completion.

---------

Co-authored-by: MoonDreamStars <moondreamstar1@gmail.com>
2026-06-23 17:30:41 +08:00
makoMakoGo 2d478876fa fix: align Claude MCP path for custom config dirs (#3431)
* fix claude mcp path for custom config dir

* fix claude mcp custom profile isolation

* fix claude mcp override path edges

* fix ci test isolation
2026-06-23 09:00:56 +08:00
Jason 92930461b7 style(proxy): apply rustfmt wrapping to legacy function_call branch
CI cargo fmt --check failed on transform_codex_chat.rs (the wrapping
introduced upstream did not match rustfmt 1.95.0). Apply cargo fmt so the
chat_legacy_function_call_to_response_item call uses the expected layout.
2026-06-22 10:59:09 +08:00
Jason c4630b5c26 feat(providers): add Volcengine Ark usage query with AK/SK signing
Query coding-plan / agent-plan usage for Volcengine Ark via the
control-plane OpenAPI (open.volcengineapi.com), which requires
account-level AccessKey signing rather than the inference API key.

- Implement Volcengine Signature V4 (an AWS SigV4 variant: fixed
  canonical header order, "HMAC-SHA256" algorithm without the AWS4
  prefix, scope ending in "request", service "ark") in
  services/coding_plan.rs
- Auto-detect the plan by probing GetAFPUsage (Agent Plan, AFP
  five-hour/weekly/monthly quotas) first, falling back to
  GetCodingPlanUsage (Coding Plan, session/weekly/monthly percentages);
  a single credential covers whichever plan the account holds
- Parse the real response shape: the window label lives in the `Level`
  field; guard ResetTimestamp <= 0 (session returns -1)
- Store account-level credentials on UsageScript (accessKeyId /
  secretAccessKey), threaded through both the test command and the
  TOKEN_PLAN auto-refresh path
- Add an independent AK/SK input block with a detailed hint pointing to
  the Volcengine console -> API Access Keys
- Add the `monthly` tier label (frontend TIER_I18N_KEYS + tray
  TIER_LABEL_GROUPS + subscription service)
- Sync zh / en / ja / zh-TW locales and the usage-query docs
2026-06-22 10:28:10 +08:00
Jason 142c8c1da7 feat(providers): rebrand CTok to ETok across presets, icons, and docs
The vendor renamed its domain, API endpoints, and trademark from CTok to
ETok. Update all user-facing branding accordingly:

- Presets (claude, claude-desktop, codex, gemini, hermes, openclaw,
  opencode): ctok.ai -> etok.ai, api.ctok.ai -> api.etok.ai, internal
  provider id ctok -> etok, display name CTok.ai -> ETok.ai
- Icons: replace inline placeholder SVG with the new ETok brand PNG,
  moved from `icons` to `iconUrls`; key ctok -> etok; banner logo
  ctok.png -> etok.png
- README (en/zh/ja/de) partner banner: new logo, links, and copy
- i18n (en/ja/zh/zh-TW): partnerPromotion key ctok -> etok

Keep "ctok" in the Codex history-migration whitelist as a legacy id and
add "etok", so existing users' local session history stays correctly
bucketed after the rename.
2026-06-22 10:28:10 +08:00
Huei Feng 455556380b Chat API: skip tool calls with missing function names (#4159)
* Chat API: skip tool calls with missing function names

Some providers send empty or absent function names in streaming
tool call deltas. Previously these produced invalid output items.

- Don't overwrite accumulated state.name with empty deltas
- Skip tool calls that never received a valid name (instead of
  falling back to 'unknown_tool')
- Apply the same defensive guard in finalize_tools and the
  non-streaming path

* Address review: defer empty-name skip to finalization

Require both call_id and name before triggering should_add,
instead of skipping eagerly when name is absent in the first
delta.  This handles providers that send id before name,
as suggested in the Codex review.

* Guard legacy function_call against empty name

Return Option<Value> from chat_legacy_function_call_to_response_item,
returning None when function_call.name is missing or empty. This covers
the legacy message.function_call path that the original guard missed.

* Remove unreachable unknown_tool fallback

---------

Co-authored-by: Jason <farion1231@gmail.com>
2026-06-19 23:38:25 +08:00
Arc 510aa250c5 add glm-5.2 pricing (#4385) 2026-06-18 23:24:20 +08:00
Jason b724f5dde9 revert(proxy): drop Anthropic system-message hoisting (#3775)
The unrelated Windows test fixes from #3775 are kept; tool-thinking-history normalization is unchanged.

Refs #4297
2026-06-16 22:39:27 +08:00
Jason 69341db284 fix(codex): probe CODEX_SQLITE_HOME when locating state DB for history migration
The session-history migration only scanned ~/.codex/state_5.sqlite and the
config.toml `sqlite_home` location. When Codex's SQLite state is relocated via
the CODEX_SQLITE_HOME env var, the state DB was never scanned and its threads
kept their old provider bucket. Add a CODEX_SQLITE_HOME fallback (config
`sqlite_home` still takes precedence) in the shared codex_state_db_paths helper
used by both the third-party and unified-session history migrations, plus serial
env tests.
2026-06-16 22:34:00 +08:00
Xinyu Kuo 0bb3b7515a feat(usage): support importing model pricing from models.dev (#4079)
* feat(usage): support importing model pricing from models.dev

Add an "Import from models.dev" button to the Add Pricing panel that
fetches https://models.dev/api.json, lists priced models sorted by
release date (newest 50 by default, full-text search across ~4800),
and bulk-imports the selected entries through the same
update_model_pricing command as manual entry.

- Normalize imported model IDs to match the backend's
  clean_model_id_for_pricing rules (strip vendor prefix, lowercase,
  truncate ':' suffix, map '@' to '-', drop the [1m] marker) so the
  stored rows actually match cost-attribution lookups
- Dedupe selections that collapse to the same model_id and report
  skipped duplicates in the success toast
- Invalidate usage queries on settled (not just success) so partial
  import failures still refresh the pricing list
- Keep ESC inside the picker's search input from closing the dialog
  and discarding the selection
- Add i18n keys for zh/en/zh-TW/ja and unit tests for the
  normalization, price formatting and flattening logic

Fixes #4017

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(usage): match scoped cost backfill against raw model aliases

The scoped backfill selected zero-cost rows via exact SQL string match,
but log columns store raw model strings (route prefixes, :free variants,
date suffixes), so alias rows were skipped until the next full backfill
on startup. Filter rows in Rust with the same model_pricing_candidates
normalization used by the pricing lookup; pricing decision logic is
untouched. Pre-existing gap from schema v11, surfaced by bulk import.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(usage): restrict models.dev pricing import to a single model

Each update_model_pricing call triggers a backfill pass that loads
every zero-cost usage row before filtering by model, so bulk-importing
N entries scaled as selectedModels x allZeroCostLogs full scans.
Re-importing pricing is rare, so drop the batch path instead of
optimizing it: the picker is now single-select, one import runs exactly
one update_model_pricing call and one backfill pass. This also removes
the normalized-ID dedup logic and the useImportModelPricing hook in
favor of the existing useUpdateModelPricing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 22:01:56 +08:00
Zhou Kai 1042fb2a32 fix(terminal): respect user shell for provider terminals (#4140)
* fix(terminal): detect user default shell instead of hardcoded bash

All macOS/Linux terminal launch functions in misc.rs hardcoded `bash` as
the execution shell, causing zsh users' .zshrc to not load. Now detect
the user's default shell via $SHELL (macOS fallback /bin/zsh, Linux
fallback /bin/bash) and use it consistently across shebang, AppleScript,
spawn args, and interactive shell flags.

- Add get_user_shell(), get_shell_name(), interactive_flags_for_shell()
- Replace 10 hardcoded bash locations with dynamic shell detection
- zsh gets --norcs --no-globalrcs, bash keeps --norc --noprofile
- run_tool_lifecycle_silently unchanged (bash-specific syntax)
- Fixes #1546, Related #2385

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(terminal): use POSIX sh for script execution, user shell for interactive session

Address code review issues:
1. Replace shell-specific `read -n 1 -s`/`read -k 1 -s` with POSIX `read -r _`
   (press Enter to close) — ensures portability across bash/zsh/fish/dash
2. Unify AppleScript to use `sh` consistently (not mixing shell/shell_name)
3. Protect fish users: scripts always interpreted by POSIX sh, not fish
   (fish doesn't support trap/exec). exec line switches to user's shell.
4. Use build_exec_line to avoid trailing space when interactive_flags is empty

All terminal launchers now execute scripts via `sh`. Script shebangs are
`#!/bin/sh`. The exec line at script end switches to the user's detected
shell with appropriate clean-start flags. Terminal launcher functions no
longer need shell/shell_name parameters — simplified signatures.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: validate user shell for terminal launch

* fix: load user shell config after terminal command

* test: align terminal shell exec expectations

* refactor(terminal): remove dead code, quote Warp path, unify shebangs

- Remove `interactive_flags_for_shell` (always returned "") and
  `get_shell_name` (no longer needed after removing flags); simplify
  `build_exec_line` to single-arg form
- Quote the Warp launcher script path with `shell_single_quote` for
  consistency with the main launcher scripts
- Unify all script shebangs to `#!/usr/bin/env sh` for better
  portability (e.g. NixOS where /bin/sh may not exist)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(terminal): fix linux shell exec helper call

* fix(terminal): run provider command through user shell

* fix(terminal): validate user shell executable

* fix(terminal): avoid interactive provider shell job

* fix(terminal): harden user shell launcher

Load zsh provider commands through the user's login and interactive environment, preserve launch cwd handling, and replace Terminal/iTerm launcher shells so the final user shell is restored cleanly.

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 16:54:25 +08:00
chen-985211 36b557b2e6 fix(codex): restore cached tool call fields (#4160)
* fix(codex): restore cached tool call fields

* refactor(codex): merge duplicate enrich loops in chat history

enrich_call_item_from_cache copied the fill-if-empty loop for
reasoning_content/reasoning. The two loops are identical and key
order is irrelevant, so fold both key sets into a single loop.

Pure refactor, no behavior change; codex_chat_history tests pass.

---------

Co-authored-by: Jason <farion1231@gmail.com>
2026-06-16 10:59:16 +08:00
Mask 3e38889ccc feat(proxy): strip effort params when thinking:disabled for DeepSeek endpoint (#4239)
DeepSeek's Anthropic-compatible endpoint rejects requests where
thinking.type=disabled coexists with effort parameters, returning
HTTP 400. This breaks Claude Code 2.1.166+ sub-agents (Workflow/Dynamic
Workflow), which hardcode thinking:disabled.

Rather than overriding thinking:disabled, remove the conflicting effort
parameters (output_config.effort / reasoning_effort) to respect Claude
Code's intent — sub-agents don't need to display reasoning.

Fixes: https://github.com/deepseek-ai/DeepSeek-V3/issues/1397

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 10:23:16 +08:00
Jason 21e695f68a chore(release): prepare v3.16.3
- Bump version to 3.16.3 (package.json, tauri.conf.json, Cargo.toml, Cargo.lock)
- Add CHANGELOG entry for v3.16.3 (59 commits since v3.16.2)
- Add trilingual release notes (zh / en / ja) under docs/release-notes/
2026-06-15 00:08:21 +08:00
Jason 5092fe51ce fix(updater): prevent codex self-update from breaking npm platform-dispatch installs
Codex ships as an npm platform-dispatch package (JS launcher @openai/codex + per-platform binary optional deps like @openai/codex-darwin-arm64). The upgrade chain ran `<bin>/codex update || <bin>/npm i -g @openai/codex@latest`, which can leave codex throwing "Missing optional dependency @openai/codex-darwin-arm64":

- `codex update` on an npm install is a bare `npm install -g @openai/codex` that prints success and exits 0 even when the platform binary fails to land, short-circuiting the `||` npm fallback.

- The npm fallback is a no-op when version==latest and only targets the main package, so it can never re-land the missing platform binary.

Fixes: (1) remove codex from prefers_official_update (Posix+Windows) so npm-managed codex no longer runs the false-success `codex update`; (2) add a runnable=false gate in installs_anchored_command emitting an uninstall+install self-heal — the only repair that re-lands the platform binary; (3) narrow by source/real to npm-managed sources (nvm/fnm/mise/homebrew, non-brew-formula) so broken brew-formula/volta/bun installs fall back to their own anchored commands instead of being mis-repaired with npm. Reuses the existing enumerate runnable signal; no new FS probing.
2026-06-14 21:21:45 +08:00
Jason 89ff2d58d1 fix(usage): improve usage-query resilience and error surfacing
- useUsageQuery: retry once + keep-last-good — show the last successful
  result for up to 10min when a query fails transiently (network/timeout/
  HTTP 5xx), so a single blip no longer flips the card to red. Deterministic
  failures (auth, empty key, unknown provider, 4xx) surface immediately and
  clear the snapshot so a stale quota can't resurface after credentials change.
- bump native balance/coding-plan/subscription request timeouts 10s -> 15s
  for slow cross-border endpoints.
- coding_plan: return explicit errors ("API key is empty" / "Unknown coding
  plan provider") instead of a blank failure, mirroring balance.
- add unit tests for keep-last-good and transient/deterministic classification.
2026-06-14 21:21:45 +08:00
Jason fee354d09e fix(health-check): disable connectivity check for official providers, restore 6s degraded threshold
Official providers (Claude/Codex/Gemini/Claude Desktop) use OAuth with an intentionally empty base_url and connect via the client's default endpoint, so cc-switch has no reliable reachability target. Probing a guessed endpoint either hits the wrong target or returns a meaningless green light. Hide the connectivity button for category === 'official'; reachability stays available for copilot/codex-oauth/third-party/custom providers, which is where the old real-request probe produced false negatives. Revert the official base_url fallback added earlier — resolve_base_url is back to extract-or-error.

The 1500ms degraded threshold was too strict; normal ~1s probe latencies showed as 'slow'. Restore the original 6000ms scale (default + config panel + per-provider range). Keep the reachability-appropriate 8s timeout / 1 retry.
2026-06-14 21:21:45 +08:00
Jason a5903d8600 feat(health-check): replace real-LLM probe with HTTP reachability check
The provider panel health check sent a real streaming model request, which many third-party providers block (401/403/WAF), causing false negatives while only stable official endpoints passed. Replace it with a lightweight reachability probe: GET the provider base_url and treat any HTTP response (200/4xx/5xx) as reachable; only DNS/connect/TLS/timeout count as failure. Latency is the probe's TTFB.

Backend (services/stream_check.rs): rewrite ~2200 -> ~350 lines, dropping real-request building, format conversion, auth and API-path resolution while keeping per-app base_url extraction. Defaults: 8s timeout, 1 retry, 1500ms degraded threshold.

Failover invariant: the reachability check must never reset the circuit breaker (reachable != usable; a 403 host is reachable but broken for real traffic). Remove the resetCircuitBreaker call from useStreamCheck; failover failure detection stays driven solely by real proxy traffic (forwarder/circuit_breaker untouched). useResetCircuitBreaker is kept dormant for a future manual-recovery entry.

Open the check to all providers: drop the official/copilot/codex-oauth/third-party gating and the 'sends a real request' confirm dialog. For official providers whose base_url is intentionally empty, fall back to the endpoint the client actually uses (Claude -> api.anthropic.com, Codex -> chatgpt.com/backend-api/codex, Gemini -> generativelanguage). Non-official providers with a missing base_url still error to avoid a false green light. Claude Desktop Official is native 1P mode (talks to claude.ai, cc-switch not in the request path, no reliable endpoint) so its button stays hidden.

Slim StreamCheckConfig and per-provider testConfig to timeout/threshold/retries (drop test model + prompt); sync zh/en/ja/zh-TW. Retain the now-unused anthropic_to_openai/anthropic_to_gemini transform utilities and their test suites.
2026-06-14 21:21:45 +08:00
thisTom 0c46efe1be fix(macos): prevent duplicate provider terminal sessions (#4156)
* fix(macos): prevent duplicate provider terminals

* fix(macos): keep Ghostty fallback on AppleScript failure

---------

Co-authored-by: thisTom <19346741+thisTom@users.noreply.github.com>
2026-06-14 16:55:23 +08:00
Jason 1992d6be72 feat: add Kimi K2.7 Code model and upgrade official Kimi presets
Add kimi-k2.7-code pricing seed (in $0.95 / out $4.00 / cache-read $0.19
per 1M tokens, 256K context) and point all six official Moonshot Kimi
presets (Claude Code, Codex, Claude Desktop, Hermes, OpenCode, OpenClaw)
at the new model. Rename the version-tagged OpenCode/OpenClaw presets to
"Kimi K2.7 Code" and correct the OpenClaw context window to 262144.

The seed is applied via the idempotent INSERT OR IGNORE path that runs on
every startup, so existing users pick up the new pricing without a schema
migration. Kimi For Coding and Nvidia presets are intentionally untouched.
2026-06-13 22:26:36 +08:00
Jason 4f8a79c273 feat: add Claude Fable 5 model mapping across Claude Code and Desktop
- Wire claude-fable-5 as a fourth tier on both proxy paths, with a
  fable -> opus -> default fallback mirroring the official downgrade.
- Whitelist the fable- prefix for the Desktop 1.12603.1+ validator.
- Clarify fallbackModelHint (zh/en/ja/zh-TW): a blank tier on
  third-party endpoints forwards the literal model name and 404s.

Refs #3980, #4026, #4049.
2026-06-13 19:12:34 +08:00
Jason eab6bfd20c feat(codex): add opt-in migration and ledger-based restore for unified session history
- Enable dialog gains a checkbox (default off) to migrate existing
  official sessions from the built-in "openai" bucket into the shared
  "custom" bucket, with per-generation backups; failed migrations retry
  at startup
- Disable dialog offers a precise restore driven by the backup ledger:
  only sessions recorded as "openai" in backups are flipped back, and
  sessions created while the toggle was on are never touched
- Completion marker and backup generations are bound to the canonical
  Codex config dir; migrate/restore serialize on an op lock and the
  marker is written conditionally inside the settings write lock
- save_settings rolls back the toggle and fails the save when the live
  rewrite fails; migration additionally requires the live config to
  actually route to the shared bucket (skips with live_not_unified so
  refused injection or proxy takeover can't split history)
- Restore refuses to run while the toggle is (re-)enabled and reports
  nothing_to_restore instead of a zero-count success; local migration
  markers are now backend-owned in merge_settings_for_save so stale
  frontend payloads can't resurrect them
- Settings autosave reverts optimistic form state on failure so a
  failed toggle change can't be replayed by an unrelated save
- ConfirmDialog supports an optional checkbox; all four locales updated
2026-06-12 23:35:01 +08:00
Jason 948d762792 feat(codex): add unified session history toggle for official providers
Codex buckets resume history by the model_provider id recorded in each
session: official runs (no key, built-in "openai") and cc-switch
third-party runs (shared "custom") are mutually invisible in the resume
picker. Add an opt-in setting that runs official providers under the
shared "custom" id so future official sessions land in the same history
bucket as third-party ones. Forward-only by design: existing sessions
are not migrated.

When enabled, official live config.toml gets model_provider = "custom"
plus a [model_providers.custom] entry that mirrors the built-in openai
provider (requires_openai_auth routes auth to the ChatGPT login in
auth.json, name "OpenAI" keeps is_openai() feature gates, explicit
supports_websockets/wire_api restore built-in defaults). auth.json is
untouched.

Key invariants:
- Injection lives only in the live config: switch-away backfill strips
  the exact injected shape, so stored provider configs stay clean and
  turning the toggle off fully reverts on the next write.
- Toggle changes apply immediately via a takeover-aware reapply: when
  the proxy owns the live config (backup/placeholder present), only the
  live backup is updated, mirroring the provider-switch path.
- The takeover backup path runs the same injection so a takeover
  release restores a config that still carries the unified routing.
- Injection refuses to activate a foreign [model_providers.custom]
  table (e.g. stale entry with a third-party base_url) to avoid routing
  ChatGPT OAuth traffic to an unknown backend.

The toggle lives under Settings → Codex App Enhancements; the
description warns that resuming old sessions across providers may fail
because encrypted_content reasoning only decrypts on the backend that
created it (upstream treats cross-provider resume as unsupported).
2026-06-12 10:40:51 +08:00
Jason a75f479576 feat(usage): lift provider/model filters to dashboard-wide scope
The provider/model filters only lived inside the request-log table, so
there was no way to see "how much did app X spend on source Y" across
the whole dashboard. Promote them to the top bar next to the app
filter, applying globally to the hero summary, trend chart, request
logs, and both stats tabs.

Backend: the five stats queries (summary, summary-by-app, trends,
provider stats, model stats) accept optional provider_name/model
filters, applied to both the detail and daily-rollup branches (the
rollup PK already carries provider_id/model/pricing_model). Sources
match by exact display name via provider_name_coalesce, so session
placeholder rows like "Claude (Session)" are selectable; models match
by effective pricing model (pricing_model falling back to model), the
same grouping key the model-stats tab uses. Request-log filtering
switches from LIKE to these exact semantics.

Frontend: two truncating dropdowns list only sources/models that have
data in the current range, with the model list cascading from the
selected source. Dynamic option values are prefix-encoded so a source
literally named "all" cannot collide with the sentinel, query keys
fall back to null instead of "all", and the option queries follow the
dashboard refresh interval (otherwise their default 30s polling drags
same-key stats queries along even with refresh disabled). The
request-log filter bar keeps only the log-specific status-code select.
Labels read "sources" rather than "providers" because direct-connect
session buckets sit alongside real providers. i18n updated across
zh/en/ja/zh-TW.
2026-06-11 22:42:26 +08:00
Jason c7efa77ad9 refactor(usage): fold claude-desktop into claude in the dashboard
The Desktop gateway's proxy traffic is still recorded under its own
app_type for route-takeover billing audit (the request detail panel
shows the real value), but the dashboard now folds it into `claude`
for display. A standalone "Claude Desktop" bucket only ever showed a
partial number: Desktop chat usage never passes through the proxy and
has no scannable local source, while its Code-tab sessions are just the
embedded Claude Code runtime writing into the shared ~/.claude/projects
tree — so a separate bucket misled users into reading it as Desktop's
full usage.

Backend: new `folded_app_type_sql` helper wraps the app_type column in
every dashboard read path (10 filter sites + the by-app projection) so
`= 'claude'` also matches claude-desktop and GROUP BY merges the two,
without changing bound-param counts. Dedup matching and provider-limit
checks keep exact comparison; get_request_logs folds only the WHERE
filter and keeps the raw app_type in its row projection.

Frontend: drop claude-desktop from the dashboard AppType/KNOWN_APP_TYPES
filter list, the UsageHero theme, and the now-dead appFilter locale key
in all four languages (the managed-app apps.claude-desktop label stays).

Adds test_claude_desktop_folds_into_claude_for_display.
2026-06-11 22:42:26 +08:00
codeasier 2d64d8c619 fix(proxy): preserve Codex OAuth auth token on takeover (#3789)
* fix(proxy): preserve Codex OAuth auth token on takeover

* style(proxy): format Codex OAuth takeover fix

* fix(proxy): unconditionally inject AUTH_TOKEN placeholder for codex takeover

The preserve-if-exists condition left #3784 unfixed on three paths:
hot-switch passes the provider's settings (presets carry no
ANTHROPIC_AUTH_TOKEN key), fresh installs never had the key, and live
configs already stripped by older releases stay stripped.

- Fold the bool parameter into the policy enum as
  ManagedAccount { keep_auth_token } so every construction site
  declares intent
- Decide via !is_github_copilot() within the managed branch so
  URL-only codex providers (no provider_type meta) are covered,
  matching the predicate family used for policy selection
- Inject the placeholder unconditionally instead of only when the
  key pre-exists; Copilot behavior is unchanged (API_KEY only)
- Pin the previously uncovered cases with tests: codex without a
  pre-existing key, URL-only codex, and Copilot removing a stale
  AUTH_TOKEN

---------

Co-authored-by: codeasier <liuyekang@huawei.com>
Co-authored-by: Jason <farion1231@gmail.com>
2026-06-11 22:21:42 +08:00
osscv d70e3828fe Add claude-mythos-5 model to schema (#4077)
Insert a new 'claude-mythos-5' model tuple into src-tauri/src/database/schema.rs. The tuple ("claude-mythos-5", "Claude Mythos 5", "10", "50", "1.00", "12.50") is added to the models list (placed before the Claude 4.8 series) to register the Mythos 5 model with the Database schema.
2026-06-11 21:53:11 +08:00
Jason Young 7bb59fa5a6 fix(proxy): harden takeover-residue recovery across config-dir switches (#4076)
Changing app_config_dir relocates the SQLite database, so a restart
triggered while proxy takeover is active used to strand the live
configs: the new instance reads a fresh DB with no live backups, the
first-run import then persisted the PROXY_MANAGED placeholder as the
`default` provider, and the no-backup recovery path wrote that
placeholder right back to the live files — leaving Claude/Codex/Gemini
pointed at a dead local proxy with no automatic way out.

Three orthogonal fixes, defense in depth:

- restart_app now awaits cleanup_before_exit() before app.restart().
  Since #4069 the ExitRequested handler intentionally defers restart
  requests to Tauri's default re-exec without custom cleanup, which is
  correct for same-DB restarts but not for this command's dir-change
  use case: only the old instance holds the backups needed to restore
  the taken-over live files, so it must restore them while its event
  loop is still alive.
- import_default_config refuses to import a live config that is under
  proxy takeover (placeholder detected), instead of persisting it as
  the current provider.
- restore_live_from_ssot_for_app validates that the current provider's
  settings_config does not itself contain takeover placeholders before
  writing it back; polluted SSOT now falls through to the placeholder
  cleanup fallback.

Regression tests cover the import guard and the no-backup recovery
path (the latter fails before this change by writing PROXY_MANAGED
back to live).
2026-06-11 21:32:24 +08:00
Jason Young 4f3e85fcd1 fix(updater): drive download/install/restart from backend to avoid hang (#4074)
* fix(updater): drive download/install/restart from backend to avoid hang

The 3.16/3.16.1 update flow was frontend-driven: downloadAndInstall()
then relaunchApp(). relaunch() routed through AppHandle::restart(), and
the old WebView had to keep running JS after the .app bundle was already
swapped — an unstable window that could hang the update or leave the old
version running until a manual restart.

Move the whole download -> install -> cleanup -> restart chain into a new
backend command install_update_and_restart, so it no longer depends on the
old WebView running JS after the bundle is swapped.

Platform-aware install ordering (install() behaves differently per OS):
- Windows: install() launches the external installer and exits the process
  internally, so cleanup + single-instance destroy must run before install.
  Surface a recovery hint on failure since the proxy may already be stopped.
- macOS/Linux: install() returns, so install first then cleanup — an install
  failure no longer wrongly stops the proxy / reverts takeover.

Eliminate the restart vs single-instance race: restart_process() destroys
the single-instance lock (remove socket on macOS, ReleaseMutex on Windows)
before tauri::process::restart(), so the freshly spawned process can't
connect to the old listener and exit itself.

Also remove the now-dead frontend update plumbing (relaunchApp,
UpdateHandle, mapUpdateHandle) and surface backend errors in the toast.

Adapted from the original af4271f4 while rebasing onto #4069: the
ExitRequested handler changes were dropped entirely — the classifier from
#4069 already routes RESTART_EXIT_CODE to Tauri's default restart flow,
and the original should_restart branch (prevent_exit + async cleanup)
would have reintroduced the window-state deadlock that #4069 fixed.
install_update_and_restart bypasses ExitRequested entirely, so the two
fixes compose cleanly.

* fix(updater): clear tray icon on the direct-restart update path

restart_process re-execs via tauri::process::restart (spawn + exit(0)),
which skips Tauri's internal cleanup_before_exit and all RunEvent::Exit
plugin hooks. Window state, proxy/live restore and the single-instance
lock were already compensated explicitly; the tray icon was not.

On macOS/Linux the OS drops the status item when the process dies, so
the gap there was cosmetic at most. The real residue risk is the
Windows branch, which never reaches restart_process at all:
update.install() exits the process inside the updater plugin
(std::process::exit(0)), bypassing TrayIcon::drop — no NIM_DELETE is
sent and a stale icon lingers in the shell until hovered, the same
failure remove_tray_icon_before_exit was originally added for on the
quit path.

Call remove_tray_icon_before_exit (set_visible(false), proxied to the
main thread via run_item_main_thread) in restart_process and before
the Windows install. Deliberately not AppHandle::cleanup_before_exit():
it drops tray icons on the calling thread, which is not safe off the
main thread on macOS (NSStatusItem).
2026-06-11 21:30:02 +08:00
thisTom a3598fd976 fix: prevent deadlock when relaunching after in-app update (#4069)
The updater's relaunch() (and app.restart()) triggers ExitRequested
with code RESTART_EXIT_CODE, which the handler treated as a regular
exit: it called api.prevent_exit() and spawned an async cleanup task.

However Tauri silently ignores prevent_exit() for restart requests
(see ExitRequestApi::prevent_exit docs), so the event loop keeps
shutting down regardless and fires every plugin's RunEvent::Exit hook.
Two threads then deadlock:

- the spawned cleanup task runs save_window_state on a tokio worker,
  holding the window-state plugin's internal mutex while querying
  window geometry, which dispatches to the main thread and blocks;
- the main thread, already inside the plugin's own RunEvent::Exit
  hook, blocks on that same mutex.

The app freezes forever on the restarting screen with the update
already installed; force-quit + reopen comes back on the new version
(#3998). Confirmed on macOS by sampling the frozen process: main
thread parked in tauri_plugin_window_state save_window_state mutex
lock, tokio worker parked in is_maximized -> mpsc recv.

Fix: classify exit requests (None / restart / user exit) and let
restart requests fall through untouched to Tauri's default restart
flow (RunEvent::Exit -> re-exec). On that path window state is saved
by the plugin's exit hook on the main thread, the tray icon is
cleaned up by Tauri's internal cleanup_before_exit, and proxy/live
config restore is unnecessary because the new instance takes over
immediately. The regular exit path (tray quit) is unchanged.

With the fix, a simulated updater relaunch (request_restart) re-execs
the new process in under 200ms, 3/3 runs; normal quit still performs
full cleanup.

Co-authored-by: thisTom <19346741+thisTom@users.noreply.github.com>
2026-06-11 17:24:06 +08:00
Jason a6d718d0fc fix(proxy): aggregate mislabeled SSE bodies in transform fallback (#2234)
The Claude/Codex format-transform non-stream branch returned an opaque 422
"Failed to parse upstream response" whenever a 2xx upstream body was not
valid JSON. The common case: MaaS gateways force-stream a stream:false
request and return an SSE body with a non-SSE Content-Type, defeating the
header-only is_sse() check.

On serde failure, sniff for SSE and aggregate the chunks into a single
JSON, then run the existing converter so clients still receive a valid
non-stream response.

- chat_sse_to_response_value: aggregate chat.completion.chunk SSE
  (content / reasoning / refusal / tool_calls / legacy function_call),
  tool_calls index-keyed via BTreeMap to avoid unbounded densification,
  first-wins finish_reason, message-snapshot override, completeness and
  error-event guards; synthesize an id when the upstream omits one
- responses_sse_to_response_value: process the residual trailing block,
  tolerating truncation and skipping it once a completed event was seen
- enrich remaining parse failures with content-type / content-encoding /
  body-snippet diagnostics
- deflate: try zlib (RFC 9110) before raw; keep the content-encoding
  header for unsupported encodings
- gate zero-usage rows on the Claude transform path
2026-06-11 08:29:29 +08:00
Jason 8b925c2f2f feat(proxy): honor custom User-Agent across stream check and model fetch
Extract a shared `parse_custom_user_agent` helper in provider.rs returning
`Result<Option<HeaderValue>>`, and reuse it in the forwarder, stream check,
and model fetch paths so detection, forwarding, and model listing all apply
the same provider-level User-Agent. Previously only the forwarder honored it,
so stream check could fail (or model listing 403) on UA-gated upstreams that
the proxy itself handled fine.

- stream_check injects the provider's custom UA on the claude/codex paths and
  still skips the GitHub Copilot fingerprint UA.
- model_fetch service + command and the model-fetch.ts wrapper thread an
  optional UA through to GET /v1/models.
- runtime callers silently ignore invalid values via `.ok().flatten()`
  (no save-time block, so deeplink imports stay lenient).
2026-06-11 08:29:29 +08:00
RoromoriYuzu ff706e9e96 feat: add provider user agent override 2026-06-11 08:29:29 +08:00
Jason feea81e5bb fix(proxy): bill route-takeover traffic by the real upstream model
The model mapped for takeover (env mapping, Claude Desktop routes,
Copilot normalization, Codex chat override) was discarded inside the
forwarder, so usage attribution depended entirely on the upstream
echoing it back. When the upstream omitted the model or mirrored the
client alias, kimi/glm tokens were recorded and priced as claude-*
(roughly 5-25x overstatement).

- capture the final outbound model in forward(), return it via
  ForwardResult, and store it on the request context
- attribution fallback order is now: upstream echo (empty string
  treated as missing) -> outbound model -> client-requested model
- 'request' pricing mode anchors to the outbound model instead of the
  pre-mapping client alias; unchanged when no mapping applies
- persist the resolved pricing_model on every usage row
- Claude Desktop rows now log app_type "claude-desktop" on streaming
  and transform paths too (was hardcoded "claude", silently dropping
  desktop provider pricing overrides and splitting the cost basis by
  the stream flag); its global pricing defaults inherit the claude
  config since proxy_config only allows claude/codex/gemini rows
2026-06-11 08:29:29 +08:00
Jason 4282856683 feat(usage): persist pricing basis and takeover dimensions in storage (schema v11)
- proxy_request_logs: add pricing_model column recording the basis actually
  used at write time (NULL = pre-v11 rows, '' = unpriced error rows)
- cost backfill recomputes strictly by the persisted basis; the
  request_model fallback now only applies to placeholder models, so
  real-but-unpriced takeover rows stay at zero cost until pricing is
  added instead of being permanently frozen at the alias's price
- backfill_missing_usage_costs_for_model can locate rows by pricing_model
- usage_daily_rollups: rebuild with request_model + pricing_model in the
  primary key so the alias-to-real-model mapping and the pricing basis
  survive the 30-day prune; legacy rows migrate with ''
- rollup_and_prune backfills costs before pruning: prune is irreversible
  and used to run before the startup backfill, permanently booking
  then-unpriced rows as zero
- get_model_stats groups by the effective pricing model
  (COALESCE(NULLIF(pricing_model,''), model)) so costs aggregate under
  the model whose prices produced them; response-mode behavior unchanged
2026-06-11 08:29:29 +08:00
Jason 65d6929993 fix(coding-plan): classify Zhipu quota windows by unit field instead of reset-time order (#3036)
The Zhipu quota API returns two TOKENS_LIMIT entries whose identity was
inferred by sorting nextResetTime ascending (nearest = five_hour). In the
last hours of each weekly cycle the weekly window resets sooner than the
current 5-hour session window, so the two buckets were swapped exactly
when users check their weekly quota most.

Classify by the explicit unit field instead (3 = hour window -> five_hour,
6 = week window -> weekly_limit; same shape on bigmodel.cn and api.z.ai,
weekly observed with number 7 and 1 so only unit is matched), falling back
to the old reset-time heuristic when the field is missing.
2026-06-11 08:29:29 +08:00
Jason 1ca01bcd10 feat(usage): refresh model pricing seed — add Fable 5 + 8 models, fix 28 prices
Full audit of seed_model_pricing against current official vendor pricing.

New models: claude-fable-5 (10/50), grok-4.3, step-3.7-flash,
mistral-medium-3.5, mistral-small-4, devstral-small-2-2512, magistral-small,
qwen3.7-max, qwen3.7-plus.

Price fixes (Chinese vendors standardized on official list price, CNY/~7.14):
- GLM 4.6/4.7 -> Z.ai official 0.6/2.2/0.11 (were reseller/OpenRouter rates)
- Grok 4.20 reasoning/non-reasoning -> 1.25/2.50 (xAI price cut)
- MiMo v2.5 / v2.5-pro / v2-pro -> post-2026-05-27 rates + cache
- Doubao Seed 2.0 lite corrected + cache-hit prices across the family
- Kimi k2.5 output 3.00, MiniMax m2.5 input 0.15, Mistral devstral-2 output 2
- Qwen 3.5/3.6-plus + coder-plus/flash cache_read (official 20%-of-input rule)

Each fix updates the seed value (fresh installs) and adds an old->new guard to
repair_current_model_pricing (existing DBs; won't clobber user-edited rows).
2026-06-11 08:29:29 +08:00