Commit Graph

92 Commits

Author SHA1 Message Date
SaladDay 0acce5477a Merge main and harden managed Codex account flow 2026-07-18 10:06:58 +00:00
Thefool 1c0ee0c58a feat(grokbuild): add first-class Grok Build support (#5453)
* feat(grokbuild): add backend integration

* feat(grokbuild): add provider and management UI

* test(grokbuild): cover configuration and integrations

* fix(grokbuild): address backend review feedback

* fix(grokbuild): complete UI review feedback

* feat(grokbuild): add CLI lifecycle management

* fix(grokbuild): align provider icon fallback

* test(grokbuild): cover provider icon persistence
2026-07-17 15:50:50 +08:00
Jason ac52c851bf fix(codex): infer image capability for generated catalogs and resync takeover live on save
Mapped GPT models were rejected by Codex clients with "model does not
support image inputs". Two root causes:

- Catalog entries for native-Responses/Anthropic providers cloned a
  template whose input_modalities defaulted to ["text"], so every mapped
  model was advertised text-only. model_catalog_json replaces Codex's
  built-in model table wholesale, and both the TUI and the IDE extension
  block images pre-send when the current model is found without "image".
- Editing the current Codex provider during proxy takeover only refreshed
  the DB backup, so removing the mapping left a stale model_catalog_json
  pointer (and its text-only catalog file) active in live config.

Changes:

- New shared model_capabilities module: explicit row declaration first,
  then a confirmed text-only registry (exact tail match only — prefix
  matching removed, variants enumerated so future -vl/-vision models fail
  open), everything else unknown.
- Catalog generation writes input_modalities from that inference for all
  tool profiles: unknown models fail open to ["text","image"]; only
  confirmed text-only models are advertised as ["text"], giving users a
  clear client-side prompt instead of silent image stripping.
- Live catalog reverse-import collapses modalities that match current
  inference, so registry corrections are not frozen into hidden row
  overrides and the rectifier's heuristic opt-out keeps working.
- Saving the current Codex provider while takeover owns live now
  re-projects the live config (mirrors the hot-switch path), so mapping
  edits and removals take effect immediately.
- Media rectifier delegates to the shared module; its preflight toggle is
  documented (4 locales) as proxy-request-only, never affecting catalog
  capability declarations.
2026-07-13 16:48:22 +08:00
Jason 5c39dfbfbe feat(codex): declare gpt-5.6 context window for Claude Code takeover
- Update Codex OAuth presets to the gpt-5.6 family (haiku -> gpt-5.6-luna)
  and bump the custom Codex template default model
- Inject CLAUDE_CODE_MAX_CONTEXT_TOKENS / CLAUDE_CODE_AUTO_COMPACT_WINDOW
  (372000, the ChatGPT Codex catalog window with a ~353K effective budget,
  openai/codex#31860) into effective live settings so Claude Code stops
  assuming a 200K window and compacts before the upstream rejects the prompt
- Gate the injected defaults on every configured model being gpt-5.6*:
  gpt-5.5's upstream catalog oscillates between 272K and 372K and must not
  inherit them; explicit user values always win
- Strip the injected values on switch-away backfill (mirror-inverse of the
  injection conditions) so program defaults never harden into per-provider
  explicit values, and keep both keys out of the shared common-config snippet
2026-07-13 16:48:21 +08:00
Jason 51d6c458ff feat(codex): route native ChatGPT sessions through proxy takeover
Allow the built-in Codex official provider to participate in takeover mode while preserving Codex's native OAuth or API-key credentials instead of persisting them into provider records.

Project official routing into a dedicated TOML provider, normalize inline tables, clean stale managed placeholders, and fail closed when the live configuration cannot be transformed safely.

Validate forwarded authorization, make official 401/403 responses non-retryable, avoid circuit-breaker pollution, and share the first-party ChatGPT endpoint across the Codex and Claude adapters.
2026-07-13 16:48:21 +08:00
Dawn 50270d5e42 fix: exclude Fable model env from Claude common config (#4272) (#5206)
* Add test for Fable model env key exclusion

Add regression test to ensure Fable model env keys are excluded from common config.

* Add Fable model to CLAUDE_MODEL_OVERRIDE_ENV_KEYS

* Add files via upload

* Delete .github/workflows/build-windows-unsigned.yml
2026-07-11 10:11:57 +08:00
zhanxin.xu 95c917b337 feat(provider): add Zhipu team plan quota query support (#5128)
智谱团队套餐(Team Plan)的额度查询与个人版不同:同一 quota 端点加
`?type=2`,需额外 `bigmodel-organization` / `bigmodel-project` 请求头
(api_key + 组织 ID + 项目 ID 三者缺一不可),且仅存在于国内站
open.bigmodel.cn。参考 token-monitor/src/shared/zaiTeamLimits.js 实现。

- Backend (services/coding_plan.rs): 新增 query_zhipu_team(固定 CN 站、
  ?type=2、org/project 头);抽出 zhipu_quota_from_body 与个人版共用解析;
  入口 get_coding_plan_quota 靠显式 coding_plan_provider == "zhipu_team"
  路由(base_url 与个人版智谱相同,detect_provider 无法区分)。新字段经
  UsageScript、IPC 命令、后台查询路径(query_provider_usage_inner)透传。
- Frontend: UsageScriptModal 新增「Zhipu GLM Team」选项 + 组织/项目 ID
  输入;模板切换时保留 team 字段;测试与保存逻辑按 team 传参。
- i18n: en/zh/zh-TW/ja 四个 locale 更新。
- Tests: 凭据校验/路由(缺任一凭据 → NotFound,标识大小写不敏感)+ 本地
  server 验证 ?type=2 与 org/project 请求头形状。

Co-authored-by: XuZhanXin <1239576606@qq.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 09:21:10 +08:00
Jason 88d5ffba44 fix(codex): move common-config TOML merge off smol-toml to backend toml_edit
updateTomlCommonConfigSnippet re-serialized the whole document through
smol-toml (parse -> deepMerge -> stringify): comments dropped, keys
reordered, and empty parent table headers synthesized -- the
long-standing "config.toml keeps getting reordered" symptom (audit C5,
introduced in 083e48bf).

Replace it with a backend command backed by the same
merge_toml_table_like / remove_toml_table_like used when writing live
configs, so the form preview and the live write share one merge
semantic and user formatting survives edit-time merges. The frontend
sync helper is deleted outright to keep the pattern from coming back.

Making the form operations async exposes them to interleaving, so a
result is discarded unless it is still current when it lands:

- a per-hook sequence number (last operation wins) covers rapid
  toggle/save races where an earlier merge resolves after a later
  removal and would flip the switch back;
- a config-baseline check covers the user hand-editing the TOML while
  a merge is in flight -- the stale result must not clobber their edit.
  The checkbox state self-heals via the existing inference effect.

Regression tests pin both by resolving a suspended merge after a newer
operation / an external edit, plus backend tests locking comment and
key-order preservation, scalar override, and value-matched removal.
2026-07-08 22:46:09 +08:00
Jason 11c173c730 fix(mcp): stop cross-app failures from blocking MCP re-projection
sync_all_enabled iterated AppType::all() with `?`, so one app's corrupt
live file (e.g. a broken ~/.claude.json, which passes the existence
gate but fails to parse) blocked every app behind it in the iteration
order -- and bubbled the error into whatever operation triggered the
sync:

- switch/save had just rewritten only the target app's live file, yet a
  broken unrelated file failed the whole operation after DB and live
  were already updated, reporting a false "switch failed" to the user.
  Both now project only the target app (sync_enabled_for_app) and
  degrade projection failure to a warning: the primary operation has
  already taken effect, and the projection self-heals on the next
  switch or MCP toggle.
- sync_current_provider_for_app_to_live syncs a single app; it now
  projects that app only, keeping failures (which can only concern the
  target app) as errors.
- sync_current_to_live (config import / cloud-sync restore) keeps the
  all-apps sweep, but sync_all_enabled is now best-effort: it projects
  every app, collects failures, and reports them aggregated. Its error
  is held until after skill sync so an MCP failure no longer skips it.
2026-07-08 22:46:09 +08:00
Jason 1f36f0cfed feat(provider): extend switch-time common-config autosync to Codex
Claude already re-extracts the live config into the shared common-config
snippet right before switching away, so shared tweaks (plugins,
preferences) made in live propagate to all opted-in providers. Codex was
gated out because its TOML pipeline leaked provider-specific and
injected content into the snippet.

With the extractor now stripping all injection artifacts and routing
fields, and backfill stripping the MCP projection, open the gate to
Claude + Codex. The autosync-before-strip ordering also self-heals
stale snippet values previously baked into provider snapshots: the
re-extracted snippet matches the live values, so the value-match strip
removes them on the same switch.

End-to-end tests cover: new shared keys captured, deletions synced,
secrets/injected artifacts never entering the snippet, and provider A's
key not leaking into provider B's live.
2026-07-08 22:46:09 +08:00
Jason 6d2ee2472f fix(provider): re-project Codex MCP after unified-session toggle rewrites live config
Toggling unify_codex_session_history rewrites the current official
provider's live config.toml in full (intended design), which drops the
[mcp_servers] projection -- and nothing put it back, so enabled MCP
servers silently vanished until the next provider switch (#C2).

Re-project after the rewrite, with two deliberate choices:

- Project Codex only (new McpService::sync_enabled_for_app) instead of
  sync_all_enabled: the all-apps sync short-circuits in AppType::all()
  order, so a corrupt ~/.claude.json would error before Codex is ever
  reached and the freshly wiped [mcp_servers] would stay missing. Only
  Codex's live file was rewritten here, so only Codex needs
  re-projection.
- Degrade projection failure to a warning: by this point the live file
  already carries the new bucket state, so the toggle has taken effect.
  Propagating the error would make save_settings roll back the setting,
  creating the exact "setting=old, live=new bucket" session split the
  rollback exists to prevent. The projection self-heals on the next
  switch or any MCP toggle.
2026-07-08 22:46:09 +08:00
Jason 473c2aaa9f fix(provider): exclude injected artifacts and routing fields from Codex common-config extraction
extract_codex_common_config kept several fields in the shared snippet
that must never cross providers:

- [mcp_servers] and the legacy [mcp.servers] form: owned by the DB
  mcp_servers table; once in the snippet they get merged into every
  opted-in provider and no sync path can ever clean them up.
- top-level experimental_bearer_token: normally lives inside
  [model_providers.<id>] (stripped with the whole table), but three
  fallbacks write it at top level -- leaking the API key into the
  shared snippet.
- model_catalog_json: per-provider catalog projection pointer.
- web_search, only when it equals the injected "disabled" sentinel;
  a user-set value remains a shareable preference.
- top-level wire_api: same provider-routing semantics as top-level
  base_url (the fallback target when no model_provider is set); leaking
  it would rewrite the next provider's protocol selection.

This makes the extractor safe as the source for switch-time
common-config autosync.
2026-07-08 22:46:09 +08:00
Allen Xu e78aa8a7c3 fix: sync openclaw and hermes live provider updates (#5098)
* fix: sync openclaw live provider updates

* fix: sync hermes live provider updates

* fix test: hermes live import stores models as array after denormalize

The test import_hermes_providers_from_live_updates_existing_provider_from_live
seeded models as a dict, but import_hermes_providers_from_live reads via
get_providers() which calls denormalize_provider_models_for_read(), converting
models from YAML dict to UI-friendly array. The test assertion accessed models
as dict -> Null -> assertion failure.

Fix: access models as array by index, matching the actual storage format after
live import. Also verify the id field is preserved in the denormalized output.
2026-07-08 12:09:56 +08:00
Allen Xu e191af4aa1 fix: OpenCode live provider import updates (#4712)
* fix: sync existing opencode providers from live config

* fix: preserve OpenCode provider display names

* docs: update comments and log messages to reflect new update behavior

- fix stale comment in lib.rs that said existing providers are skipped
- change log message from 'Imported' to 'Synced' since count now
  includes both new imports and updates
2026-07-08 10:34:49 +08:00
秋澪Akimio b3e5e32c89 feat: add Claude subagent model config (#4830)
* feat: add Claude subagent takeover config

* feat: add Claude subagent model field

* i18n: add Claude subagent model labels

* fix(proxy): preserve configured subagent model mapping

* fix(providers): exclude subagent model from Claude common config

* style: format rust code
2026-07-07 10:28:36 +08:00
SaladDay d6ebc24cb3 fix(codex-oauth): keep managed token in restore backup; fix failing tests
CI caught three test failures from the previous commit. Root cause: the Live
backup (proxy_live_backup) is local restore state that is replayed to
~/.codex/auth.json when proxy takeover ends, so it must contain the real auth to
restore a working login. Stripping it (the earlier "don't leak into backup"
change) broke restore and over-stripped a user's native login.

- Revert the backup auth stripping: remove sanitize_codex_backup_auth from the
  initial/strict snapshot paths and the forced auth={} in
  update_live_backup_from_provider_inner. The managed token belongs in the
  restore backup (the refresh_token is already persisted by CodexOAuthManager, so
  this is not a new exposure).
- Restore the ownership marker (account_id + access_token fingerprint) so backup
  cleanup can tell our managed write from a user's native `codex login` of the
  same account. extract now also tolerates the full-bundle shape (refresh_token +
  last_refresh) so it fingerprints ①'s refreshable writes. clear_codex_auth_in_backup
  and clear_codex_live_auth_for_managed_account use the marker again; the
  account_id-only helper is dropped.
- Fix the adopt unit test: adopt now invalidates the cached access token, so the
  test asserts the stored refresh_token/id_token were updated and the cache was
  cleared instead of reading it back through get_valid_token_bundle (which would
  trigger a network refresh).

Token stays out of the exported provider settings_config (backfill strip) — only
the local restore backup keeps it, matching pre-existing behavior.
2026-07-01 15:43:49 +00:00
SaladDay c72bd49213 fix(codex-oauth): make managed accounts safe & usable with the bare CLI
Addresses review feedback on #3879. These backend changes are deeply
intertwined across shared files, so they are committed together.

Refreshable managed auth (bare `codex` support):
- Write a full, native-shaped auth.json bundle (tokens.refresh_token + top-level
  last_refresh) instead of an access-only token, so the Codex CLI can self-refresh
  and the managed account keeps working past ~1h without a proxy.
- last_refresh reflects the access token's real obtained-at time (not write time),
  so the CLI doesn't treat a cached token as freshly refreshed.
- Read back the CLI-rotated refresh_token from ~/.codex/auth.json before writing
  (account_id-matched, chatgpt-mode guarded) so re-switching doesn't clobber the
  CLI's valid login with a stale refresh_token.

Never persist managed tokens at rest:
- Backfill of a managed provider always replaces live auth with the stored
  placeholder (even on marker mismatch), so native tokens can't leak into the
  provider's DB config.
- Sanitize managed auth out of Live backups on the initial/strict snapshot paths
  and in update_live_backup_from_provider_inner before serialization.

Concurrency / blocking:
- Drop the redundant outer Arc<RwLock<CodexOAuthManager>> (all methods are &self)
  so token refresh no longer holds a coarse lock across the network; give OAuth
  token/device requests a 30s timeout instead of the shared 600s client default.
- Add a persistence lock and linearize the access-token cache under a consistent
  accounts -> access_tokens order (existence-checked reads/writes; remove/clear
  clear the cache atomically) to prevent stale or resurrected cache entries.

Switch ordering:
- Preflight the managed token before committing the current provider on
  switch/add/update, so a failed token fetch can't leave DB/UI pointing at a
  provider whose live config was never written.

Cleanup:
- Remove the now-dead content-fingerprint marker machinery; managed writes are
  now shape-identical to a native login, so ownership is judged by account_id.

Known, documented limitations (B-scheme, narrow & recoverable): a remove+re-login
of the same account within an in-flight refresh window (ABA), a login authorized
after logout, and switch-away cleanup being unable to distinguish our write from a
user's native login of the same account.
2026-07-01 15:19:12 +00:00
Jason ef731c9919 Merge remote-tracking branch 'origin/main' into feature/managed-oauth-account-selector 2026-07-01 17:04:59 +08:00
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
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
SaladDay fc5fa85807 feat(codex): write managed ChatGPT id_token into live auth + reauth prompt
Bind the selected ChatGPT account's id_token into the Codex official
managed auth.json so it matches a native browser login for the fields
that drive behavior (auth_mode, account/plan/email via id_token claims).

- Persist id_token on CodexAccountData; capture at login and on refresh
  (empty string treated as missing). Add get_valid_token_and_id_token_for_account.
- codex_managed_oauth_live_auth now writes tokens.id_token when available.
- Keep the managed-vs-native safety marker intact: extract_codex_managed_oauth_auth
  tolerates id_token but still rejects native logins (which carry refresh_token /
  top-level last_refresh), so cc-switch never clears a real browser login.
  refresh_token and last_refresh are intentionally NOT written for this reason.
- Surface reauth_required (account has no stored id_token) through
  GitHubAccount -> ManagedAuthAccount -> TS. Legacy accounts get a styled
  amber prompt + one-click re-login (device flow) in CodexOAuthSection,
  flagged in both the selector dropdown and the select-mode hint.
- i18n: reauth strings added for en / ja / zh / zh-TW.

Verified: pnpm typecheck + build:renderer green; cross-model review of Rust
correctness, the managed-vs-native safety invariant, and the id_token lifecycle.
2026-06-23 16:59:24 +00:00
SaladDay 0aeca9a235 Merge remote-tracking branch 'origin/main' into feature/managed-oauth-account-selector
# Conflicts:
#	src/components/providers/forms/ProviderForm.tsx
2026-06-23 15:54:43 +00: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 7cbdae9eca Merge remote-tracking branch 'origin/main' into feature/managed-oauth-account-selector
# Conflicts:
#	src-tauri/src/services/provider/live.rs
#	src-tauri/src/services/proxy.rs
2026-06-15 15:04: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
saladday 7480cec0ba Add managed ChatGPT account binding for Codex 2026-06-08 04:39:53 +08:00
Jason b7499fc871 Refresh Codex provider label on proxy takeover hot-switch
During proxy takeover, switching third-party Codex providers left the
client-visible provider name stale: sync_codex_live_from_provider_while_proxy_active
based the live config on the existing live file and only patched
base_url/wire_api/model, never refreshing model_provider or
model_providers.<id>.name. The Codex app kept showing the previous
provider in its bottom-right label.

Rebuild the effective settings from the DB for the selected provider so
the live config carries the correct provider key and display name, then
merge MCP servers back from the existing live config. base_url stays
pointed at the local proxy, and official OAuth in auth.json is untouched
(takeover writes config.toml only when auth preservation is enabled).

Generalize preserve_codex_mcp_servers_in_backup ->
preserve_codex_mcp_servers_from_existing_config since it now serves both
the backup and live-sync paths.
2026-06-01 15:16:13 +08:00
Jason 2a131a5572 Harden Codex takeover ownership signaling and serialize switch/takeover
Gate provider sync and switching on the restore backup / live placeholder
("is this live file owned by takeover?") instead of the lagging
proxy_config.enabled and proxy-running flags. The backup is created
before enabled=true is committed, so during that activation window the
old guards were blind and a concurrent sync/switch could rewrite the
taken-over live file, clearing Codex auth.json for a mis-categorized
provider.

Acquire a per-app switch lock around both set_takeover_for_app and
provider switching so the two cannot interleave, splitting the locking
entry points into outer (lock) / inner (no-lock) pairs to stay
deadlock-free. Preserve the official OAuth auth in provider-rebuilt
restore backups by routing the provider token into config.toml. Refine
takeover idempotency to require the live config to point at the current
proxy URL, rebuilding from backup when it does not.

Add unit and integration tests covering the official -> DeepSeek ->
takeover on/off lifecycle and the stopped-proxy switch path.
2026-06-01 11:41:05 +08:00
MelorTang 05ba28016c fix: sync Claude Desktop profile during proxy takeover (#3157)
* fix: sync Claude Desktop profile during proxy takeover

* fix(provider): skip Claude Desktop backup refresh during takeover

- Route Claude Desktop takeover updates directly through the 3P profile writer.
- Keep takeover startup backup state unchanged when provider metadata changes.
- Narrow platform-specific test helpers and environment setup with cfg gates.

* fix(provider): restore PathBuf import for CI tests

- Restore an unconditional PathBuf import for provider tests.

- Keep Linux cargo test builds compiling while preserving Claude Desktop cfg-gated helpers.

---------

Co-authored-by: Jason <farion1231@gmail.com>
2026-05-26 23:01:33 +08:00
Jason 95f2dd4126 feat(codex): preserve OAuth login state during third-party provider switching
Codex provider switches now only write config.toml for third-party providers,
injecting the API key as experimental_bearer_token. The user's auth.json
(ChatGPT OAuth tokens) is preserved. Official providers with login material
still write auth.json normally. Backfill restores bearer tokens into stored
provider auth.OPENAI_API_KEY to maintain canonical shape.
2026-05-25 22:20:34 +08:00
Jason f93b935d5f fix(proxy): expose real provider model names in Claude Code menu under takeover
When proxy takeover is active, write per-role *_MODEL aliases for routing
and *_MODEL_NAME with the upstream provider's real model name so the
Claude Code model menu reflects the active provider instead of stale
display names from a previous switch. Preserves the [1M] capability marker
for Sonnet/Opus, and strips it from implicit display names.
2026-05-14 12:16:00 +08:00
Jason 402570ce31 fix(usage): pricing routing, SSE lifecycle, and validation hardening
* model pricing routing: extend prefix-match families (gpt-/o1-o5/
  gemini-/deepseek-/qwen-/glm-/kimi-/minimax-) with per-family dash
  thresholds so short base IDs like gpt-5 no longer mis-match
  gpt-5-mini; strip ISO and 8-digit date suffixes via UTF-8-safe
  byte matching so claude-haiku-4-5-20251001 falls back to
  claude-haiku-4-5 pricing
* SSE collector: SseUsageFinishGuard (RAII) guarantees finish() on
  early return or panic; AtomicBool fast path lets push() skip the
  Mutex once first-event time is recorded
* validation: shared validate_cost_multiplier / validate_pricing_source
  helpers across DAO and service layers; PRICING_SOURCE_RESPONSE /
  PRICING_SOURCE_REQUEST constants replace string literals; price
  fields in update_model_pricing now reject empty / non-decimal /
  negative input before INSERT
* backfill: add backfill_missing_usage_costs_for_model so a single
  price edit only scans matching rows instead of the full log table;
  startup backfill remains full-scan
* session_usage{,_codex,_gemini}: share find_model_pricing helper from
  usage_stats; metadata_modified_nanos centralizes mtime precision
* frontend: NON_NEGATIVE_DECIMAL_REGEX + isNonNegativeDecimalString
  replace three copies of the same multiplier regex; isUnpricedUsage
  surfaces zero-cost rows that have usage tokens (cached per row to
  avoid double evaluation); invalidate usageKeys.all on pricing mutate
  so backfilled rows refresh
2026-05-14 11:53:51 +08:00
Jason 8b3ad9caf9 feat(claude-desktop): add 3P provider switching with proxy gateway
Adds a new ClaudeDesktop AppType that writes Claude Desktop's third-party
inference profile under configLibrary/, sharing _meta.json with other
launchers (Ollama-compatible) so cc-switch can coexist with them.

Two switch modes:
- direct: provider already exposes claude-* / anthropic/claude-* model
  ids on Anthropic Messages, Claude Desktop connects to it directly.
- proxy: cc-switch's local proxy acts as the inference gateway,
  presenting only claude-* route names to Claude Desktop and mapping
  them to real upstream models. Required after Anthropic restricted
  Claude Desktop to claude-family ids.

Backend:
- New module claude_desktop_config with snapshot/rollback, official seed
  bypass, /claude-desktop/v1/{models,messages} routes, and a single
  source of truth for default proxy routes.
- Gateway token persisted in SQLite, validated on every proxied request.
- get_claude_desktop_status surfaces drift signals (stale models,
  missing routes, proxy stopped, base URL mismatch, missing token).

Frontend:
- Slim ClaudeDesktopProviderForm independent from ProviderForm,
  controlled by a top-level appId guard.
- ProviderList banner consumes the status query (5s polling) and
  renders actionable diagnostics.
- ClaudeDesktopRouteToggle in the header to start/stop the local
  gateway without touching takeover state.
- Three-locale i18n synchronised.
2026-05-08 22:34:46 +08:00
Dhruv_S b05be92aa1 Fix Codex startup live import duplication (#2590)
* Fix Codex startup live import duplication

* Fix: Prevent duplicate Codex default provider on restart & add startup import tests
2026-05-08 08:41:50 +08:00
Jason f935bac633 feat(hermes): bind per-provider models to top-level model: on switch
Hermes custom_providers entries now carry an ordered models array
(id / context_length / max_tokens) plus suggestedDefaults. The backend
serializes the array to the YAML dict shape Hermes expects on write and
inverts it on read, preserving insertion order via the preserve_order
feature on serde_json.

When a user switches providers, switch_normal calls apply_switch_defaults
so the top-level model.default / model.provider follow the selected
provider's first model. Previously switching a Hermes provider only
shuffled custom_providers[] and left Hermes pointing at whatever
model.provider was set before.

Seven existing Hermes presets now ship with a curated models list so
switching lands on a working default without a detour through the
Model panel.
2026-04-21 11:57:06 +08:00
Jason 240969d8c7 feat: add Hermes UI components, presets, and config panels (Phase 8)
- Add 7 provider presets (OpenRouter, Anthropic, OpenAI, Google, DeepSeek, Together, Nous)
- Create HermesFormFields + useHermesFormState for provider form integration
- Create Model/Agent/Env config panels with save/load functionality
- Create HermesHealthBanner for config warnings
- Add hermes icon (violet winged H) to icon system
- Integrate into App.tsx: 3 new view types (hermesModel/hermesAgent/hermesEnv),
  sidebar buttons (Brain/Bot/KeyRound), health banner, session support
- Integrate into ProviderForm: presets, form state, key validation, rendering
- Integrate into AddProviderDialog: universal tab exclusion, providerKey, base_url extraction
- Add i18n keys for all Hermes UI (zh/en/ja)
2026-04-21 11:57:06 +08:00
Jason 6d0e9f4c74 feat: implement Hermes config module and commands (Phase 3)
Add hermes_config.rs (~1190 lines) with YAML section-level replacement
that preserves comments and formatting in unmanaged sections:
- Type definitions: HermesModelConfig, HermesAgentConfig, HermesEnvConfig
- YAML section finder (find_yaml_section_range) with column-0 key detection
- Provider CRUD on custom_providers array (indexed by name field)
- Model/Agent config get/set via yaml<->json conversion
- .env dotenv read/write preserving comments and line ordering
- Health check, backup with rotation, write lock (OnceLock<Mutex>)
- MCP section access stubs for Phase 4
- 19 unit tests

Add commands/hermes.rs with 10 Tauri commands registered in lib.rs.
Replace all Hermes TODO stubs in services/provider/live.rs with real
implementations (import, remove, write-to-live, read-live-settings).
2026-04-21 11:57:06 +08:00
Jason 81af0a57f9 feat: add Hermes Agent as 6th supported app type (Phase 1)
Register AppType::Hermes across the entire Rust backend:
- Add Hermes variant to AppType enum with additive mode and MCP support
- Add hermes field to McpApps, SkillApps, CommonConfigSnippets, and all
  per-app structs (McpRoot, PromptRoot, VisibleApps, AppSettings)
- Create minimal hermes_config.rs with get_hermes_dir() respecting
  settings override, matching the pattern of other app config modules
- Update all match arms in commands, services, deeplink, proxy, mcp,
  session_manager, and test files
- Extract shared build_additive_app_settings() to eliminate duplication
  between OpenClaw and Hermes deep link handling
- Combine identical OpenClaw/Hermes proxy match arms into unified arms
2026-04-21 11:57:06 +08:00
Jason e4c34b34e7 fix: remove ANTHROPIC_REASONING_MODEL to decouple thinking from model selection (#2081)
ANTHROPIC_REASONING_MODEL was a non-official env var that forced all
requests with thinking params to use a single "reasoning model",
overriding the user's /model selection. Since new Claude Code versions
send adaptive thinking by default, this caused /model to silently fail.

- Remove reasoning_model field and has_thinking_enabled() from model_mapper
- Simplify map_model() to pure type-based matching (haiku/sonnet/opus)
- Remove reasoning model UI field from provider form
- Retain ANTHROPIC_REASONING_MODEL in ENV_EXCLUDES and override-key
  cleanup lists so legacy configs don't leak into common config
2026-04-21 11:57:06 +08:00
Jason a514f27937 feat: block official provider switching during proxy takeover
Prevent users from switching to official providers (Anthropic/OpenAI/Google)
when proxy takeover is active, as using a proxy with official APIs may cause
account bans.

Defense-in-depth across 4 layers:
- Backend: ProviderService::switch(), hot_switch_provider(), switch_proxy_provider command
- Frontend: useProviderActions soft guard with error toast
- UI: ProviderActions button disabled with ShieldAlert icon
- Tray menu: official provider items disabled with  indicator

Also warns when enabling proxy takeover while current provider is official.
2026-04-14 10:33:41 +08:00
Jason 8b44d9f54d fix(test): resolve HOME env race condition in parallel tests
- Use get_home_dir() instead of dirs::home_dir() in get_opencode_dir()
  and get_openclaw_dir() to respect CC_SWITCH_TEST_HOME override
- Add CC_SWITCH_TEST_HOME to all TempHome implementations
- Add #[serial] to all with_test_home tests to share serialization
  with other env-mutating tests
- Remove --test-threads=1 workaround from CI
2026-04-02 22:08:12 +08:00
Jason bfe238f301 style: fix formatting issues caught by CI 2026-04-02 17:32:33 +08:00
Roy Li ba0b57f014 fix(claude): sync takeover live config during provider changes (#1828)
Keep Claude's live settings aligned with the latest provider state while proxy takeover is active, without breaking takeover fields or restore behavior.

Co-authored-by: Jason <farion1231@gmail.com>
2026-04-01 22:21:59 +08:00
Dex Miller 3553d05bf0 feat(provider): additive provider key lifecycle & fix openclaw serializer panic (#1724)
* feat(provider): support additive provider key lifecycle management

Add `addToLive` parameter to add_provider so callers can opt out of
writing to the live config (e.g. when duplicating an inactive provider).
Add `originalId` parameter to update_provider to support provider key
renames — the old key is removed from live config before the new one
is written.

Frontend: ProviderForm now exposes provider-key input for openclaw app
type, and EditProviderDialog forwards originalId on save. Deep-link
import passes addToLive=true to preserve existing behavior.

* test(provider): add integration tests for additive provider key flows

Cover openclaw provider duplication scenario to verify that a generated
provider key is assigned automatically. Add MSW handlers for
get_openclaw_live_provider_ids, get_openclaw_default_model,
scan_openclaw_config_health, and check_env_conflicts endpoints.
Update EditProviderDialog mock to pass originalId alongside provider.

* fix(openclaw): replace json-five serializer to prevent panic on empty collections

json-five 0.3.1 panics when pretty-printing nested empty maps/arrays.
Switch value_to_rt_value() to serde_json::to_string_pretty() which
produces valid JSON5 output without the panic. Add regression test for
removing the last provider (empty providers map).

* style: apply rustfmt formatting to proxy and provider modules

Reformat chained .header() calls in ClaudeAdapter and StreamCheckService
for consistent alignment. Reorder imports alphabetically in stream_check.
Fix trailing whitespace in transform.rs and merge import lines in
provider/mod.rs.

* style: fix clippy warnings in live.rs and tray.rs

* refactor(provider): simplify live_config_managed and deduplicate tolerant live config checks

- Change live_config_managed from Option<bool> to bool with #[serde(default)]
- Extract repeated tolerant live config query into check_live_config_exists helper
- Fix duplicate key generation to also check live-only provider IDs
- Fix updateProvider test to match new { provider, originalId } call signature
- Add streaming_responses test type annotation for compiler inference

* fix(provider): distinguish legacy providers from db-only when tolerating live config errors

Change `ProviderMeta.live_config_managed` from `bool` to `Option<bool>`
to introduce a three-state semantic:
- `Some(true)`: provider has been written to live config
- `Some(false)`: explicitly db-only, never written to live config
- `None`: legacy data or unknown state (pre-existing providers)

Previously, legacy providers defaulted to `live_config_managed = false`
via `#[serde(default)]`, which silently swallowed live config parse
errors. This could mask genuine configuration issues for providers that
had actually been synced to live config before the field was introduced.

Now, only providers with an explicit `Some(false)` marker tolerate parse
errors; legacy `None` providers surface errors as before, preserving
safety for already-managed configurations.

Also wrap the `ensureQueryData` call for live provider IDs during
duplication in a try/catch so that a malformed config file shows a
user-facing toast instead of silently failing.

Add tests for both the legacy error propagation path and the frontend
duplication failure scenario.

* refactor(provider): unify OMO variant updates with atomic file-then-db writes and rollback

Consolidate the duplicated omo/omo-slim update branches into a single
match on the variant. Write the OMO config file from the in-memory
provider state *before* persisting to the database, so a file-write or
plugin-sync failure leaves the database unchanged. If `add_plugin`
fails after the config file is already written, roll back to the
previous on-disk contents via snapshot/restore.

Also:
- `sync_all_providers_to_live` now skips db-only providers
  (`live_config_managed == Some(false)`) instead of attempting to write
  them to live config.
- `import_{opencode,openclaw}_providers_from_live` mark imported
  providers as `live_config_managed: Some(true)` so they are correctly
  recognized during subsequent syncs.
- Extract OmoService helpers: `profile_data_from_provider`,
  `snapshot_config_file`, `restore_config_file`, `write_profile_config`,
  and the new public `write_provider_config_to_file`.
- Add 9 new tests covering sync skip, legacy restore, import marking,
  OMO persistence, file-write failure, and plugin-sync rollback.

* fix(provider): fix additive provider delete/switch regressions and redundancy

- fix(delete): replace stale live_config_managed flag check with
  check_live_config_exists so providers written to live before the
  flag-flip logic was introduced are still cleaned up on delete
- fix(switch): make write_live_with_common_config return Err instead of
  silently returning Ok when config structure is invalid, preventing
  live_config_managed from being incorrectly flipped to true
- fix(update): block provider key rename for OMO/OMO Slim categories to
  prevent orphaned current-state markers breaking OMO file syncs
- fix(switch): flip live_config_managed to true after successful live
  write for DB-only additive providers so sync_all_providers_to_live
  includes them on future syncs; roll back live write if DB update fails
- refactor(delete): merge symmetric OMO/OMO-Slim blocks into single
  match-on-variant path; hoist DB read to top of additive branch
- refactor(remove_from_live_config): merge OMO/OMO-Slim if/else-if
  into single match-on-variant path
- refactor(switch_normal): merge two OMO/OMO-Slim if blocks into one
  OpenCode guard with (enable, disable) variant pair
- fix(update): remove redundant duplicate return Ok(true) after OMO
  current-state write

* fix(test): use preferred_filename after OMO field rename

The merge from main brought in #1746 which renamed
OmoVariant.filename → preferred_filename, but the test helper
omo_config_path() was not updated, breaking compilation of all
new provider tests.

---------

Co-authored-by: Jason <farion1231@gmail.com>
2026-04-01 21:16:41 +08:00
Jason af8f907467 fix(proxy): serialize per-app provider switches to prevent state corruption
Concurrent failover switches for the same app could cause is_current,
local settings, and Live backup to point at different providers.

- Add SwitchLockManager with per-app mutexes (different apps still parallel)
- Unify scattered switch logic into ProxyService::hot_switch_provider
- Fix TOCTOU in set_current_provider via mutate_settings
- Add logical_target_changed to skip redundant UI refreshes
- Add tests for serialization and restore-waits-for-switch scenarios
2026-03-31 22:46:42 +08:00
Jason 7ca33ff901 fix: prevent common config loss during proxy takeover and stabilize snippet lifecycle
- Make sync_current_provider_for_app takeover-aware: update restore
  backup instead of overwriting live config when proxy is active
- Introduce explicit "cleared" flag for common config snippets to
  prevent auto-extraction from resurrecting user-cleared snippets
- Reorder startup: extract snippets from clean live files before
  restoring proxy takeover state
- Add one-time migration flag to skip legacy commonConfigEnabled
  migration on subsequent startups
- Add regression tests for takeover backup preservation, explicit
  clear semantics, and migration flag roundtrip
2026-03-12 17:16:22 +08:00
Jason e561084f62 Preserve common config during proxy takeover
Update takeover backup generation to rebuild effective provider settings with common config applied before saving restore snapshots.

Keep Codex mcp_servers entries when hot-switching providers under takeover so restore does not drop live-only MCP config.

Migrate legacy providers with inferred common-config usage to explicit commonConfigEnabled=true markers during startup and default imports, and cover the new behavior with proxy and provider regression tests.
2026-03-12 16:06:07 +08:00
Jason 9092e97bc2 style: format Rust code with cargo fmt 2026-03-08 22:27:53 +08:00
Jason 6d078e7f33 feat: apply common config as runtime overlay instead of materialized merge
Common config snippets are now dynamically overlaid when writing live
files, rather than being pre-merged into provider snapshots at edit time.
This ensures that updating a snippet immediately takes effect for the
current provider and automatically propagates to other providers on
their next switch.

Key changes:
- Add write_live_with_common_config() overlay pipeline
- Strip common config from live before backfilling provider snapshots
- Normalize provider snapshots on save to keep them snippet-free
- Add explicit commonConfigEnabled flag in ProviderMeta (Option<bool>)
- Migrate legacy providers on snippet save (infer flag from subset check)
- Add Codex TOML snippet validation in set_common_config_snippet
- Stabilize onConfigChange callbacks with useCallback in ProviderForm
2026-03-08 21:45:20 +08:00
Jason 859f413756 revert: restore full config overwrite + Common Config Snippet (revert 992dda5c)
Revert the partial key-field merging refactoring introduced in 992dda5c,
along with two dependent commits (24fa8a18, 87604b18) that referenced
the now-removed ClaudeQuickToggles component.

The whitelist-based partial merge approach had critical issues:
- Non-whitelisted custom fields were lost during provider switching
- Backfill permanently stripped non-key fields from the database
- Whitelist required constant maintenance to track upstream changes

This restores the proven "full config overwrite + Common Config Snippet"
architecture where each provider stores its complete configuration and
shared settings are managed via a separate snippet mechanism.

Reverted commits:
- 24fa8a18: context-aware JSON editor hint + hide quick toggles
- 87604b18: hide ClaudeQuickToggles when creating
- 992dda5c: partial key-field merging refactoring

Restored:
- Full config snapshot write (write_live_snapshot) for Claude/Codex/Gemini
- Full config backfill (settings_config = live_config)
- Common Config Snippet UI and backend commands
- 6 frontend components/hooks for common config editing
- configApi barrel export and DB snippet methods

Removed:
- ClaudeQuickToggles component
- write_live_partial / backfill_key_fields / patch_claude_live
- All KEY_FIELDS constants
2026-02-27 23:12:34 +08:00