mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-16 13:34:04 +08:00
main
801 Commits
-
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>
chen-985211 ·
2026-06-16 10:59:16 +08:00 -
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>
Mask ·
2026-06-16 10:23:16 +08:00 -
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.
Jason ·
2026-06-14 21:21:45 +08:00 -
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.Jason ·
2026-06-14 21:21:45 +08:00 -
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.
Jason ·
2026-06-14 21:21:45 +08:00 -
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.
Jason ·
2026-06-14 21:21:45 +08:00 -
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>
thisTom ·
2026-06-14 16:55:23 +08:00 -
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.
Jason ·
2026-06-13 22:26:36 +08:00 -
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.
Jason ·
2026-06-13 19:12:34 +08:00 -
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
Jason ·
2026-06-12 23:35:01 +08:00 -
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).
Jason ·
2026-06-12 10:40:51 +08:00 -
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.
Jason ·
2026-06-11 22:42:26 +08:00 -
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.
Jason ·
2026-06-11 22:42:26 +08:00 -
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>
codeasier ·
2026-06-11 22:21:42 +08:00 -
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.osscv ·
2026-06-11 21:53:11 +08:00 -
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).
Jason Young ·
2026-06-11 21:32:24 +08:00 -
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).
Jason Young ·
2026-06-11 21:30:02 +08:00 -
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>
thisTom ·
2026-06-11 17:24:06 +08:00 -
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
Jason ·
2026-06-11 08:29:29 +08:00 -
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).
Jason ·
2026-06-11 08:29:29 +08:00 -
RoromoriYuzu ·
2026-06-11 08:29:29 +08:00 -
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
Jason ·
2026-06-11 08:29:29 +08:00 -
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
Jason ·
2026-06-11 08:29:29 +08:00 -
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.
Jason ·
2026-06-11 08:29:29 +08:00 -
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).
Jason ·
2026-06-11 08:29:29 +08:00 -
fix(proxy): extend image rectifier to Codex /responses text-only path
Codex /responses requests routed to text-only OpenAI-chat upstreams (e.g. DeepSeek deepseek-v4-flash) failed with HTTP 400 "unknown variant image_url" when images were sent: the responses->chat conversion turns input_image items into image_url blocks the model rejects. The media rectifier previously covered only the Claude adapter, so neither the proactive strip nor the reactive retry fired for Codex. - media_retry_should_trigger: accept "Codex" adapter, not just "Claude" - contains_image_blocks / replace_images: also scan responses `input` (input_image) in addition to chat `messages` - is_image_block_type: match image | image_url | input_image - is_unsupported_image_error: add "unknown variant" hint for the deserialize error - forward(): proactively run apply_media_prevention for Codex after the responses->chat conversion Proactively strips images for known text-only models (heuristic on by default) and reactively retries with images replaced on upstream image-unsupported errors. Adds tests for chat image_url, codex input_image, the reactive trigger, and the deserialize error match.
Jason ·
2026-06-11 08:29:29 +08:00 -
fix(proxy): exclude cache_read and cache_creation from input on Claude←OpenAI paths
Builds on #2774 (which fixed cache_read for the streaming openai_chat path). Two gaps remained, both double-counting cache tokens when a Claude client meters as app_type="claude" (input_includes_cache_read=false): 1. cache_read was still added to input on the non-streaming openai_chat path (transform.rs openai_to_anthropic) and the whole openai_responses family (transform_responses.rs build_anthropic_usage_from_responses, covering the non-streaming call site and both streaming_responses call sites). 2. cache_creation was never subtracted on any converted path, including the streaming openai_chat path #2774 had already touched. Claude billing treats cache_creation as a separate bucket, so an inclusive upstream carrying a direct cache_creation_input_tokens field billed it twice. All four metering points now compute: input = prompt_tokens - cache_read - cache_creation restoring the invariant input + cache_read + cache_creation == prompt_tokens. Pure OpenAI upstreams are unaffected (no cache_creation concept/field). Tests: update direct-cache assertions (40->20), add a streaming conservation regression test, and pin prompt<cache underflow (saturating clamp to 0) for all three metering functions. cargo test 1573 pass, clippy clean. Note: fix is forward-only; historical rows are not recomputed (cost is frozen at log time and app_type="claude" mixes native + converted rows).
Jason ·
2026-06-11 08:29:29 +08:00 -
fix(proxy): correct usage accounting on format-conversion paths
Audited all proxy format-conversion paths (Chat<->Message, Chat<->Response, Gemini<->Message) for usage/cache metering. Five issues found and fixed. The dedup mechanism (request_id PK, proxy/session source isolation) is untouched, so no double-counting is introduced. - A (Claude + openai_chat, streaming): inject stream_options.include_usage so OpenAI-compatible upstreams emit usage in the SSE tail. Without it the converted Anthropic message_delta was all-zero and the whole request's input/output/cache was dropped. Same root cause as the already-fixed Codex Chat path; the injection is extracted into a shared helper (transform::inject_openai_stream_include_usage) reused by both paths. - C (Claude + gemini_native): subtract cachedContentTokenCount from input_tokens in build_anthropic_usage so input becomes fresh input (Anthropic semantics). Previously the cache-hit tokens were billed twice because this path meters as app_type="claude" (input_includes_cache_read = false) while Gemini's promptTokenCount includes the cache. - D (Codex + openai_chat, streaming): gate log_usage on has_billable_tokens() to skip the synthetic all-zero usage the converter emits when a non-compliant upstream omits usage, preventing empty-row request-count inflation. - P2 (from_claude_stream_events): use has_billable_tokens() for the return gate instead of input>0||output>0, so a fully-cached streamed request (cache_read>0, input==output==0) is still recorded. Affects all Claude-streaming paths, not just Gemini. - P3 (Codex Chat->Responses, non-streaming): apply the same has_billable_tokens() filter the streaming branch got, since the synthesized all-zero usage makes from_codex_response return Some and bypass the `if let Some` guard. Add TokenUsage::has_billable_tokens() as the unified predicate. New tests cover include_usage injection, gemini input subtraction, the gate itself, cache-only stream recording, and synthetic all-zero codex usage. Full lib suite: 1569 passed.
Jason ·
2026-06-11 08:29:29 +08:00 -
fix(usage): import billable session messages without stop_reason
The local session-log scanner dropped any assistant message that lacked a stop_reason or had output_tokens==0. Claude Code Workflow / sub-agent fan-out frequently produces messages that only wrote a message_start snapshot (output=1, stop_reason=None) without a final block, yet their input + cache_read + cache_creation tokens are already billed by Anthropic (charged once the request is accepted). Dropping them under-counted usage by ~4.1% overall, 92% concentrated in workflow/subagent transcripts. Replace the stop_reason/output gate with a billable-token check (any of input/output/cache_read/cache_creation > 0). The per-message-id dedup selection is unchanged, and request_id = "session:"+msg_id PRIMARY KEY with INSERT OR IGNORE keeps each message single-inserted, so relaxing the gate cannot double-count. Add a regression test covering a stop_reason-less message with real cache cost plus an all-zero skip. This is the parser-layer half of the Workflow under-counting fixed at the collector layer in 8d332925.
Jason ·
2026-06-11 08:29:29 +08:00 -
fix(usage): count Claude Code Workflow sub-agent token usage
collect_jsonl_files only walked <project>/<session>/subagents/*.jsonl, so it missed Workflow sub-agent transcripts which live one level deeper at subagents/workflows/wf_*/agent-*.jsonl. As a result all Workflow token usage was invisible to the no-proxy session-log accounting. Descend into subagents/workflows/wf_*/ as well, via a new push_jsonl_children helper that keeps the fixed-depth, no-recursion design. journal.jsonl carries no assistant rows so it is skipped at parse time and needs no filename special-casing. Existing dedup (request_id PK + INSERT OR IGNORE + should_skip_session_insert) keeps the next sync's backfill idempotent. Add test_collect_jsonl_files_includes_workflow_subagents.
Jason ·
2026-06-11 08:29:29 +08:00 -
fix: usage script provider credential resolution (#1479)
The JS-script usage path resolved {{apiKey}}/{{baseUrl}} with env-only field guessing, so apps that store credentials elsewhere (Codex: auth.OPENAI_API_KEY + config.toml base_url) always got empty values and custom-template queries failed despite a fully configured provider. - query_usage / test_usage_script now delegate to Provider::resolve_usage_credentials, the same per-app resolver used by the native balance/coding-plan path and mirrored by the frontend getProviderCredentials; explicit non-empty script values still win - test_usage_script loads the provider and applies the same fallback, so testing matches what a saved script does - the custom-template variable preview shows the effective values (script overrides first, then provider config) instead of always showing provider credentials - extract_codex_base_url documents and test-locks the frontend-mirror invariant: non-active [model_providers.*] sections are never read Reworked from the original patch to reuse the existing resolver instead of duplicating per-app extraction. Co-authored-by: Jason <farion1231@gmail.com>pa001024 ·
2026-06-10 22:57:27 +08:00 -
fix: prevent duplicate YAML keys in Hermes config (#3267)
* fix: prevent duplicate YAML keys in Hermes config Three changes in hermes_config.rs: 1. deduplicate_top_level_keys() - scan and remove duplicate top-level keys before YAML parsing, preventing "duplicate entry" parse errors 2. remove_all_sections() - helper to strip all occurrences of a given top-level key from raw YAML text 3. replace_yaml_section() now calls remove_all_sections() on the remainder after replacing the primary occurrence, preventing duplicate sections from accumulating on repeated writes Fixes the issue where mcp_servers (or any top-level key) gets duplicated in config.yaml, causing "Failed to parse Hermes config as YAML: duplicate entry with key" errors. Co-Authored-By: que3sui <204201112+que3sui@users.noreply.github.com> * fix: handle CRLF and LF line endings in top-level key deduplication is_top_level_key_line only accepted empty, space, or tab after the colon, but deduplicate_top_level_keys uses split_inclusive('\n'), so lines end with \n (LF) or \r\n (CRLF). Without accepting \r and \n as valid post-colon characters, the dedup safety net never activates. Add \r and \n checks to is_top_level_key_line, and three tests covering LF, CRLF, and first-occurrence preservation. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(hermes): keep last occurrence when healing duplicate YAML keys Reworks the healing layers on top of the CRLF root-cause fix: - deduplicate_top_level_keys: keep the LAST occurrence of each duplicated key instead of the first. Duplicates come from section replacement degrading into appends (#3633), so the last block is the newest data -- and Hermes itself reads the config with PyYAML, whose duplicate-key semantics are last-wins. Keeping the first occurrence would silently roll users back to stale config and diverge from what Hermes runs with. Healthy files take a fast path and are returned untouched. - Drop the unused dup_key variable (fails cargo clippy -- -D warnings, which CI enforces). - replace_yaml_section: clean residual duplicate sections from the remainder via remove_all_sections; values come from the keep-last healed read, so dropping all stale on-disk copies loses nothing. - Add regression tests for the actual root cause (find/replace on CRLF input must replace in place, not append), keep-last semantics, identity on healthy files, end-to-end heal-then-parse, and duplicate cleanup on write. Fixes #3633 #2973 #2529 #3310 #3762 --------- Co-authored-by: que3sui <204201112+que3sui@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Jason <farion1231@gmail.com>
que3sui ·
2026-06-10 21:42:54 +08:00 -
修复 Completions转Anthropic时不记录实际返回模型、Input token记录错误问题 (#2774)
* fix(proxy): 修复completions转claude格式流式响应未记录实际命中模型 * style: cargo fmt fix * fix(proxy): 修复completions转claude格式时input与cache_read重复计费 * fix(proxy): 修复完全缓存命中时input_tokens计算错误 * test: 更新input_tokens期望值匹配去重逻辑
LaoYueHanNi ·
2026-06-09 20:30:05 +08:00 -
fix(proxy): strip cache_control from OpenAI format conversion (#3841)
* fix(proxy): strip cache_control from OpenAI format conversion (#3805) - Remove cache_control passthrough from system messages, text blocks, and tools to prevent 400 errors on strict OpenAI-compatible endpoints - Always simplify single text block content to plain string format - Fixes two format conversion bugs reported in issue #3805 * fix(proxy): apply cargo fmt to fix CI formatting check
cc10143 ·
2026-06-08 12:38:39 +08:00 -
feat(proxy): map input_file and input_audio content parts to chat
Convert Responses input_file (requiring file_id or file_data, never file_url which Chat file parts do not support) and input_audio parts into their Chat Completions equivalents, and handle top-level input_* items that previously fell through and were dropped, clearing stale pending reasoning for non-assistant messages.
Jason ·
2026-06-07 20:56:35 +08:00 -
fix(proxy): distinguish truncated chat streams from normal completion
Replace the unconditional finalize at chat-to-responses stream end with a three-way guard: complete normally when finish_reason or [DONE] arrived, emit an incomplete response when substantive output exists without a finish_reason, and emit a failed (stream_truncated) event for empty truncation instead of masking it as completed. Also propagate late-arriving reasoning_content onto still-active tool-call items.
Jason ·
2026-06-07 20:56:35 +08:00 -
fix(proxy): cache reasoning across turns for custom_tool_call and tool_search_call
Generalize the cross-turn reasoning cache in codex chat history from function_call only to the full tool-call triad (function_call, custom_tool_call, tool_search_call) and their *_output counterparts, so apply_patch and tool-search calls keep their reasoning_content when restored via previous_response_id.
Jason ·
2026-06-07 20:56:35 +08:00 -
fix(proxy): resolve actual port for ephemeral (port 0) listen config
When listen_port is 0 the OS assigns the port at bind time, so the configured value can no longer be trusted for building takeover URLs. - server: read listener.local_addr() after bind and propagate the actual port to the global proxy port, status, and ProxyServerInfo - services: start the proxy before takeover when port is 0 so live configs get the real port instead of :0, and persist the resolved port back to the DB for DB-only URL paths; stop the pre-started server on any takeover failure - claude_desktop: reject an unresolved :0 port instead of emitting a broken gateway URL - build_proxy_urls: prefer the running server's port and error out if the port is still 0 Add tests for takeover with an ephemeral port and the claude_desktop :0 rejection; switch existing codex takeover tests to an ephemeral port for isolation.
Jason ·
2026-06-07 20:56:35 +08:00 -
feat(proxy): add GET /v1/models endpoint for Codex CLI reachability check (#3818)
* feat(proxy): add GET /v1/models endpoint for Codex CLI reachability check Codex CLI probes GET /v1/models at startup. Without this endpoint the proxy returns 404, causing Codex to fail before any request reaches the upstream LLM. Return an OpenAI-compatible model list derived from the cc-switch–managed model catalog file. Fixes #3812 * fix(proxy): return Codex catalog schema from /v1/models Codex deserializes the response as a catalog with a top-level `models` field, not the OpenAI `{"object":"list","data":[...]}` envelope. Return the catalog file content directly so the format matches what Codex expects. Co-authored-by: Codex review bot * fix(proxy): guard /v1/models against serving stale catalog Only return the model catalog when config.toml still references it via `model_catalog_json`. After switching to a provider without a custom catalog, the old file lingers on disk — serving it unconditionally would advertise the previous provider's models to Codex. Co-authored-by: Codex review bot * fix(proxy): match relative model_catalog_json in stale-guard cc-switch writes `model_catalog_json = "cc-switch-model-catalog.json"` (relative) via set_codex_model_catalog_json_field. Match on the filename constant rather than the absolute path so the guard works with both relative and absolute paths. Co-authored-by: Codex review bot * fix(proxy): parse model_catalog_json field instead of substring match Replace raw config_text.contains() with proper TOML field parsing so commented-out lines and stray mentions of the filename in other fields don't defeat the stale guard. Also switch from contains() to exact filename match (Path::new(val).file_name() == Some(...)) to stay consistent with resolve_cc_switch_catalog_path in codex_config.rs. Add log::debug! when the guard blocks serving so the operator can distinguish "no models configured" from "guard blocked stale catalog". * refactor(proxy): reuse resolve_cc_switch_catalog_path in handle_models Replace the inline config.toml parsing and filename match in handle_models with the existing resolve_cc_switch_catalog_path helper (now pub(crate)). This removes the duplicated stale-guard logic, keeps a single source of truth for catalog-path ownership, and makes the handler honor absolute model_catalog_json paths the same way Codex live-setting import does. --------- Co-authored-by: Jason <farion1231@gmail.com>
CSberlin ·
2026-06-07 20:26:44 +08:00 -
[codex] Fix VS Code session previews (#3593)
* Fix Codex VS Code session previews * fix(codex): use last IDE request heading for session previews A markdown heading inside the active selection / open file could precede the real injected request, so matching the first "## My request for Codex:" heading picked selection content instead of the user prompt. Scan for the last matching heading (the IDE injects the real request as the final section) on both the Rust title path and the frontend TOC preview path. Add regression tests for the selection-heading case, and pin the known best-effort limitation when the request body itself repeats the heading. --------- Co-authored-by: Jason <farion1231@gmail.com>
ayxwi ·
2026-06-07 19:23:24 +08:00 -
fix: normalize path separators in scan_dir_recursive for Windows (#3430)
On Windows, Path::strip_prefix produces backslash-separated relative paths. The update-check matching logic uses rsplit('/') to extract the install name, so subdirectory skills (e.g. skills/my-skill) never matched and updates were silently skipped. Replace backslashes with forward slashes when building the directory string.c9 ·
2026-06-07 17:57:37 +08:00 -
阿珏 ·
2026-06-06 22:27:39 +08:00 -
Dearli666 ·
2026-06-05 22:50:49 +08:00 -
fix(usage): correct inflated input_tokens in Claude stream parsing
Some Anthropic-compatible SSE providers (e.g. qwen, minimax) report the full context (fresh + cached) as input_tokens in message_start, double counting the cached portion that is also reported in cache_read_input_tokens. This inflated the cacheable-input denominator and pushed the displayed cache hit rate artificially low. When a message_delta carries a smaller positive input_tokens, prefer it over the message_start value and adopt the cache counts from the same usage block to avoid double counting; fall back to the start cache values when the delta omits them. Native Claude (no input in delta) and OpenRouter-converted (input only in delta) paths are unchanged. Refs #3580
Jason ·
2026-06-05 21:45:34 +08:00 -
feat(usage): add official subscription quota template with unified tier rendering
Changes: - Add official_subscription template type for Claude/Codex/Gemini - Replace implicit 'category=official auto-query' with explicit opt-in template - Default disabled; users enable via usage script modal with configurable interval - Unify tier→label mapping across subscription and script paths via labeled_tier_parts() - Fix tray rendering: week aliases (seven_day/opus/sonnet) now use highest utilization - Add depth guard: official_subscription checks enabled flag in query_provider_usage_inner - Add cache invalidation symmetry: invalidate_subscription() for disabled providers - i18n: add templateOfficialSubscription + hint in zh/en/ja/zh-TW Backend (Rust): - provider.rs: add TEMPLATE_TYPE_OFFICIAL_SUBSCRIPTION branch, flatten SubscriptionQuota→UsageData - tray.rs: extract labeled_tier_parts() shared by both summary functions, use max_by for multi-alias groups - usage_cache.rs: add invalidate_subscription() method - Test coverage: add week-alias highest-utilization tests for both paths Frontend (TypeScript): - UsageScriptModal: add official_subscription to templates, auto-detect for official providers - ProviderCard: gate useUsageQuery with !isOfficialSubscriptionUsage, pass autoQueryInterval to footer - SubscriptionQuotaFooter: accept autoQueryInterval prop, default 0 (disabled) - constants.ts: add TEMPLATE_TYPES.OFFICIAL_SUBSCRIPTION Fixes tier rendering regression where: - Claude/Codex: seven_day was missed (only weekly_limit matched) → lost 7-day window in tray - Gemini: gemini_pro/flash/flash_lite fell through to fallback → leaked machine names - Multi-window (opus+sonnet): find() took first, not worst → underestimated utilization and emoji color All tests pass (cargo test + cargo clippy clean).
Jason ·
2026-06-05 19:03:40 +08:00 -
阿南 ·
2026-06-04 23:32:32 +08:00 -
fix(coding-plan): route Zhipu quota query to the user's configured base URL (#3702)
Fixes #3701. `query_zhipu` was hard-coded to `https://api.z.ai`, so a user who configured the mainland China preset (`Zhipu GLM` on `open.bigmodel.cn`) could not retrieve usage once the international endpoint became unreachable from their network (or vice versa). The two endpoints share the same quota path (`/api/monitor/usage/quota/limit`) and return JSON in the same shape, and — crucially — each user only ever uses one of them: the quota host is the same host they're already running coding on. So we can route by the configured `base_url` and skip the cross-host fallback entirely. What this PR changes -------------------- A single helper that maps the user's `base_url` to the matching quota host, and `query_zhipu` rebuilt to take `base_url` and pick the right host: fn zhipu_quota_base(base_url: &str) -> &'static str { if base_url.contains("bigmodel.cn") { "https://open.bigmodel.cn" } else { "https://api.z.ai" } } async fn query_zhipu(base_url: &str, api_key: &str) -> SubscriptionQuota { let url = format!( "{}/api/monitor/usage/quota/limit", zhipu_quota_base(base_url), ); // ... original 401/403 -> Expired / make_error / parse path, unchanged } The dispatcher already distinguishes `ZhipuCn` from `ZhipuEn` via `detect_provider()` and routes the call through `query_zhipu(base_url, api_key)` in the same match arm. Why no cross-host fallback -------------------------- Farion's review pointed out that adding a fallback would be over-engineered and actively harmful: 1. Reachability is determined by the preset the user chose. Their configured host is the host they are already using to run coding; if it were unreachable, the user could not have reached the "query usage" step at all. 2. The fallback path required distinguishing "both 401/403" (genuine bad key) from "one 401/403 + one network error" (regional block), which silently misclassified the second case as a generic query failure and hid the upstream "Session expired" UX for invalid keys. 3. It also cost the worst-case ~10s+10s≈20s serial timeout for users on a working primary. With the URL-based routing in place, 401/403 returns to the original `CredentialStatus::Expired` semantics — same UX as `query_kimi` and `query_minimax`. Files changed ------------- - `src-tauri/src/services/coding_plan.rs` — 1 file, +35 / -20 Testing ------- - 3 new `zhipu_quota_base_*` routing tests - 15 existing `coding_plan` parser tests still pass - `cargo fmt --check` clean - `cargo clippy --lib --no-deps -- -D warnings` clean Co-authored-by: Yongmao Luo <yongmao.luo@columbia.edu> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Yongmao Luo ·
2026-06-04 23:10:45 +08:00 -
fix(proxy): skip backup/restore when Live is already a proxy placeholder (#3689)
When previous stop_with_restore() failed to restore the user's original Live (e.g. app crash mid-stop, settings.json unwritable, or any pre-existing state where Live carries the proxy placeholders), the next start_with_takeover would read the still-placeholder Live and overwrite the good backup row with the proxy config itself. After that, every subsequent stop would restore the proxy placeholder back to Live — making the proxy toggle a no-op and leaving the client pinned at http://127.0.0.1:15721. Fix: in both backup write paths (`backup_live_configs` and `backup_live_config_strict`) detect that Live is already a proxy placeholder and skip the save, preserving any existing good backup. In `restore_live_config_for_app_with_fallback_inner`, detect the same condition in the parsed backup and fall through to the existing SSOT (current provider DB) path that was added in
c3d810a. Both sides share a new `live_has_proxy_placeholder_for_app` dispatch helper so the placeholder check stays in lockstep with the existing per-app detection functions. Co-authored-by: Yongmao Luo <yongmao.luo@columbia.edu> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>Yongmao Luo ·
2026-06-04 22:54:56 +08:00 -
feat(usage): add OpenCode session usage sync (#3215)
* feat(usage): add OpenCode session usage sync Add OpenCode as a fourth app type in the usage statistics system. Reads per-message token data from opencode's local SQLite database (~/.local/share/opencode/opencode.db) and imports into proxy_request_logs. - New session_usage_opencode.rs module following Codex/Gemini pattern - Parses assistant message.data JSON for tokens, cost, model - Adds "opencode" to AppType union and filter tabs - Updates dedup filters to include opencode_session data_source - Adds i18n keys for all 4 locales * fix(usage): add opencode to UsageHero title themes * fix: respect XDG_DATA_HOME and platform defaults for OpenCode DB path - Support OPENCODE_DB env var override (absolute and relative paths) - Use ~/Library/Application Support/opencode/ on macOS - Use XDG_DATA_HOME/opencode/ when set - Fall back to ~/.local/share/opencode/ on Linux - Rename misleading test to test_parse_message_data_ignores_role * fix(usage): use ~/.local/share/opencode on all platforms OpenCode relies on xdg-basedir, which ignores macOS/Windows conventions, so its DB always lives at ~/.local/share/opencode. The previous macOS default pointed at ~/Library/Application Support/opencode, which does not exist, making the sync a silent no-op for macOS users without XDG_DATA_HOME set. * fix(usage): include opencode -wal mtime in freshness check OpenCode runs its SQLite DB in WAL mode; new commits land in the -wal file and the main DB file's mtime only advances on checkpoint. Keying the freshness gate solely on opencode.db could skip newly written sessions until a checkpoint occurred. Take the max of the db and -wal mtimes instead. * style(usage): apply cargo fmt to opencode session sync Fixes Backend Checks cargo fmt --check failure. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(usage): ignore empty OPENCODE_DB and XDG_DATA_HOME env vars An empty OPENCODE_DB collapsed the path to the data dir (dropping the opencode.db filename); an empty XDG_DATA_HOME produced a relative "opencode" path. Treat empty strings as unset, per the XDG spec. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(usage): harden OpenCode session sync error handling and labeling - Map '_opencode_session' provider_id to 'OpenCode (Session)' display name - Return accurate inserted flag from insert_opencode_message (was always true) - Do not advance file/session sync_state when a session errors, so failed inserts are retried next run instead of being permanently skipped - Surface per-message insert failures into the sync result errors - Add opencode_session data-source icon and i18n labels (zh/zh-TW/en/ja) - Add provider-stats labeling test for opencode session rows * fix(usage): only import finalized OpenCode messages An in-progress assistant message holds partial tokens; OpenCode updates the same message_id with final values later. Since request_id is fixed and the insert uses INSERT OR IGNORE, a partial row could never be corrected. Skip messages without time.completed so each turn is imported once with final usage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(usage): keep OpenCode incomplete sessions retryable --------- Co-authored-by: Eira Hazel <kip3vx9ma@mozmail.com> Co-authored-by: Eria hazel <git config --global user.email your@email.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Jason <farion1231@gmail.com>
@nothingness0db nothingness0db ·
2026-06-04 22:26:24 +08:00 -
feat: 新增 S3 兼容云存储同步 (#1351)
* Add S3 Cloud Sync design document Design for adding AWS S3 as a new Cloud Sync backend alongside WebDAV. Hybrid approach: extract shared sync protocol, add independent S3 transport. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add S3 cloud sync implementation design (reqwest + Sig V4) Updated design based on 2026-03-06 draft: switches from rust-s3 crate to hand-rolled AWS Sig V4 on existing reqwest for broader S3-compatible service support (AWS, MinIO, R2, Alibaba OSS, Tencent COS, Huawei OBS). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add S3 cloud sync implementation plan (11 tasks, TDD) Detailed step-by-step plan covering: sync_protocol extraction, S3 Sig V4 transport, settings, sync/auto-sync modules, Tauri commands, frontend presets/dynamic form, and i18n. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * deps: add hmac crate for S3 Sig V4 signing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: extract sync_protocol.rs from webdav_sync.rs for shared use Move transport-agnostic sync protocol logic (constants, types, snapshot building, manifest validation, artifact verification, snapshot application, utilities) into a new shared sync_protocol module so both WebDAV and the upcoming S3 transport can reuse it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use transport-neutral error keys in sync_protocol Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3 transport layer with AWS Sig V4 signing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3SyncSettings to AppSettings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3 sync module with upload/download/fetch Implements the S3 sync protocol layer (s3_sync.rs) that combines the shared sync_protocol with the S3 transport. Mirrors the WebDAV sync module structure with independent sync mutex, connection check, upload, download, fetch_remote_info, and sync status persistence. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3 auto sync worker with debounce Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3 sync Tauri commands and auto sync worker startup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3 sync TypeScript types and API layer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3 sync i18n translations (en/zh/ja) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add S3 sync presets and dynamic form to sync settings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: preserve HTTP scheme for S3 custom endpoints (MinIO support) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add live S3 integration tests (env-var driven, --ignored) Run with: S3_TEST_AK=... S3_TEST_SK=... S3_TEST_BUCKET=... cargo test --lib services::s3::integration_tests -- --ignored Verifies test_connection, put_object, get_object, head_object, and 404 handling against a real S3 bucket using the project's own Sig V4 signing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: remove internal design docs before PR * fix: wire S3 auto-sync to DB hook & sync UI state on async load - P1: Add s3_auto_sync::notify_db_changed call in SQLite update_hook so S3 auto-sync worker receives DB change signals (was only wired for WebDAV, leaving S3 worker idle) - P2: Add useEffect to update syncType selector when s3Config loads asynchronously, preventing stale "webdav" default for S3 users * fix: satisfy clippy for s3 sync * fix: address s3 sync review feedback --------- Co-authored-by: Keith (via OpenClaw) <keithyt06@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Jason <farion1231@gmail.com>
Keith Yu ·
2026-06-04 22:18:51 +08:00