Commit Graph

317 Commits

Author SHA1 Message Date
Jason 940161fb0e feat(providers): add routing support badges for Claude Code and Codex
Add visual indicators for routing capabilities on provider cards:
- Claude Code: "Needs Routing" badge for non-official providers with non-anthropic API formats
- Claude Code: "No Routing Support" badge for official providers
- Codex: "No Routing Support" badge for official providers

The badges help users understand which providers support format conversion through routing.
2026-05-14 17:51:42 +08:00
Jason 543e057e20 fix(providers): disable model test for third-party Claude providers
Most third-party Claude Code providers now reject requests from
non-official clients, so the model test button would just produce
noisy failures (or worse, trigger risk controls on the provider
side). Treat third-party Claude providers the same way as official /
Copilot / Codex OAuth: pass onTest=undefined so ProviderActions
renders the test button in its existing disabled visual state.
2026-05-14 15:26:54 +08:00
Jason 73bc4eb65d feat(codex-oauth): fetch model list from ChatGPT backend on demand
- Add `get_codex_oauth_models` Tauri command reusing the managed OAuth
  access token to hit `chatgpt.com/backend-api/codex/models`; HTTP and
  multi-shape JSON parsing live in `services::codex_oauth_models` so the
  command stays thin.
- Unify the Claude form's "fetch models" button across normal / Copilot /
  Codex OAuth presets, drop the auto-load effect for Copilot in favor of
  explicit clicks, and guard against stale responses with a requestId ref.
- Add Vitest coverage for both Copilot and Codex OAuth paths asserting no
  request on mount and the correct account id on click; add Rust unit
  tests for the four model-list payload shapes.
2026-05-14 15:03:31 +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 4b57f7e113 feat(claude-code): role-based model mapping with display names and 1M flag
- Replace the four flat env inputs with a Sonnet/Opus/Haiku role table.
  Each row exposes ANTHROPIC_DEFAULT_*_MODEL plus a new display name
  field ANTHROPIC_DEFAULT_*_MODEL_NAME, and Sonnet/Opus gain a
  "Declare 1M" checkbox that toggles the [1M] suffix.
- Strip the [1M] context-capability marker before forwarding non-Copilot
  requests upstream. Copilot keeps its existing [1m]->-1m normalization.
- Claude Desktop import now consumes ANTHROPIC_DEFAULT_*_MODEL_NAME as
  label_override, closing the Claude Code -> Claude Desktop displayName
  pipeline; add_route's merge logic is shared between hashmap branches.
- Unify the [1M] marker as ONE_M_CONTEXT_MARKER across
  claude_desktop_config and proxy::model_mapper; rename the strip
  helper to strip_one_m_suffix_for_upstream.
- Collapse useModelState's seven duplicated useState initializers and
  the useEffect parse block into a single parseModelsFromConfig call.
- Add tests/hooks/useModelState.test.tsx and a Claude Desktop import
  test covering Kimi K2 -> label_override. i18n (en/ja/zh) updated.
2026-05-13 17:06:05 +08:00
Jason 84bac6dce6 refactor(claude-desktop): lock route IDs to sonnet/opus/haiku roles
Adapt to Claude Desktop 1.6259.1+ fail-all validation which only
accepts claude-(sonnet|opus|haiku)-* route IDs. Branded model names
(DeepSeek, Kimi, GLM, etc.) now live in a new labelOverride field
instead of being embedded in route IDs.

- Backend auto-repairs legacy unsafe routes to the next free
  sonnet/opus/haiku slot instead of erroring
- Frontend swaps the free-form route input for a role dropdown plus
  menu display name field
- Add CLAUDE_DESKTOP_ROLE_ROUTE_IDS as the single source of truth
  for role-to-route mapping; presets and form both consume it
- Drop the dead displayName alias on ClaudeDesktopModelRoute and the
  ineffective /v1/models display_name injection (UI ignores it)
- Update i18n (en/ja/zh) and form focus test for the new fields
2026-05-13 15:22:23 +08:00
Jason c12364a940 feat(claude-desktop): rework Claude Code import flow
- Derive route keys from the upstream model name (pass-through style)
  instead of fixed Claude aliases, and translate the legacy [1M] suffix
  into the supports1m field at the import boundary. Three Claude aliases
  mapped to the same upstream now collapse to a single route (e.g.
  MiniMax-M2 across SONNET/OPUS/HAIKU env produces one
  claude-MiniMax-M2 -> MiniMax-M2 row), with [1M] OR-aggregated.
- Add an import-time safety net that rebuilds claude-desktop-official
  when missing, so users who deleted it can recover via the normal
  import button without losing customizations on other providers.
- Hide API key and endpoint URL inputs in the official provider edit
  form to mirror Claude Code's behavior and prevent user confusion.
- Reword the empty-state import button label for clarity.
2026-05-12 21:31:30 +08:00
Jason 60a3628360 refactor(claude-desktop): replace [1M] suffix with supports1m field
inferenceModels entries now emit {name, supports1m: true} objects when
1M is enabled (plain strings otherwise), instead of appending a " [1M]"
suffix to model IDs. Route IDs and upstream model IDs are stored
verbatim; the suffix is rejected on input rather than silently stripped,
and proxy request mapping now requires an exact route_id match.
2026-05-12 17:40:32 +08:00
Jason 44d4ea81af - 修复 Claude Desktop 模型输入框失焦
- 为动态模型行添加稳定 rowId,避免编辑模型 ID 时重挂载

- 增加模型映射和直连模型列表焦点保持回归测试
2026-05-12 11:47:45 +08:00
Jason 6a3c2fe0ba - 支持 Claude Desktop 使用 Copilot/Codex OAuth 供应商
- 放开本地路由托管 OAuth 供应商校验,允许动态 Token
- 新增 Claude Desktop Copilot/Codex 预设与账号选择
- 添加 OAuth proxy 回归测试
2026-05-12 11:30:11 +08:00
Jason 953b7cdcf9 refactor(claude-desktop): drop displayName from model route schema
Claude Desktop's new model menu reads model IDs directly and ignores the
display_name field, so a separate displayName slot added UI noise without
any product value. Collapse the routeId / model / displayName tuple down
to routeId / model, and let the route ID carry the user-visible name
through a non-editable claude- prefix rendered next to the input.

Drop display_name from ClaudeDesktopModelRoute, ClaudeDesktopDefaultRoute,
and ResolvedModelRoute on the Rust side plus the matching TS interfaces,
stop emitting it in /v1/models responses, derive route IDs from upstream
model IDs when picked via the model dropdown, and update zh/en/ja copy to
describe the new two-field layout.
2026-05-12 10:46:35 +08:00
Jason 5bbd83f7ca feat(claude-desktop): add 44 provider presets translated from Claude Code
- New src/config/claudeDesktopProviderPresets.ts with the Claude Code
  preset list re-shaped into Desktop's three-segment route format
  (routeId / upstreamModel / displayName); excludes OAuth providers,
  AWS Bedrock (no SigV4 support) and KAT-Coder (placeholder URL).
- Non-Claude upstream presets show upstream model id as displayName
  (e.g. deepseek-v4-pro) so the Desktop model list reflects what is
  actually being requested. OpenRouter/TheRouter/PIPELLM keep
  Sonnet/Opus/Haiku since their upstream really is Anthropic Claude.
- Wire ProviderPresetSelector into ClaudeDesktopProviderForm so
  selecting a preset back-fills baseUrl, mode, routes and apiFormat.
- Drop the hard-coded ANTHROPIC_AUTH_TOKEN write in handleSubmit so
  ANTHROPIC_API_KEY presets (LemonData / AiHubMix / Gemini Native)
  save under the correct env key, and clear the opposite key on switch.
- Hide the universal-providers tab for claude-desktop because its
  meta-driven routing has no analogue in the universal flat-env shape.
- Add apps."claude-desktop" i18n key (zh/en/ja) so the dialog tab
  label resolves instead of showing the literal key.
2026-05-09 17:19:09 +08:00
Jason 309f7609a5 refactor(claude-desktop): show badge only for providers requiring routing
Direct-mode providers no longer display a badge since routing is
optional for them. Proxy-mode providers now show "需要路由" / "Requires
routing" to clarify that local routing must be active.
2026-05-08 22:34:46 +08:00
Jason 21b9eb0430 refactor(claude-desktop): simplify model mapping UX
- Remove "Import from Claude" button from main provider list (keep in empty state)
- Remove "Desktop model" column from proxy mode mapping table; route names are now auto-generated
- Rename "upstream model" label to "requested model" and equalize column widths
- Rewrite model mapping hint and toggle description for end-user clarity
- Update validation messages and remove dead i18n keys (routeModelLabel)
2026-05-08 22:34:46 +08:00
Jason 2deee1097b refactor(claude-desktop): align provider form UI with Claude Code
- Rename field labels: "Gateway Base URL" → "API Endpoint", "Bearer Token" → "API Key"
- Change layout from 2-column grid to vertical sections matching Claude Code
- Reuse shared EndpointField component with format-aware amber hint box
- Replace native <datalist> with vendor-grouped ModelDropdown (OpenCode pattern)
- Move Fetch/Add buttons to section headers with compact sm styling
- Extract ModelDropdown to shared module, deduplicate from OpenCodeFormFields
- Extract renderActionButtons helper to eliminate proxy/direct button duplication
- Remove dead i18n keys (gatewayBaseUrl, bearerToken) from all 3 locales
2026-05-08 22:34:46 +08:00
Jason 34698723e3 fix(claude-desktop): remove proxy-stopped status alert
The route toggle in the top-right corner already communicates proxy
state; the extra warning banner was redundant and inconsistent with
other managed apps.
2026-05-08 22:34:46 +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
makoMakoGo 064b339bab fix(claude): persist max effort via env (#2493) 2026-05-01 17:41:32 +08:00
Jason 5b6339d78c chore(codex): hide 1M context window toggle in provider edit form
Codex models no longer accept model_context_window=1000000, so the
toggle and its paired auto-compact-limit input are commented out in
the provider edit form. State hooks, helper imports, and i18n keys
are preserved so the UI can be restored in one batch if upstream
support returns. The TOML editor remains visible, allowing manual
edits if needed.
2026-04-29 22:41:46 +08:00
allen_xln 85f0be9e1d feat(provider-form): soften validation with "save anyway" prompt (#2307)
* feat(provider-form): soften business-rule validation with "save anyway" prompt

Refactor handleSubmit so empty-field / missing-item validations (provider
name, endpoint, API key, opencode model, template variables, provider key
required) no longer hard-reject with toast.error. Instead they are collected
into an issues list and presented via a ConfirmDialog; the user can cancel
or choose "Save anyway" to proceed.

Integrity constraints stay as hard rejections:
- providerKey regex / duplicate (would corrupt other providers)
- Copilot / Codex OAuth not authenticated (no token, cannot establish)
- omo Other Fields JSON not an object / parse failure

This aligns the frontend with the backend's existing "relaxed save / strict
switch" split (see gemini_config.rs: validate_gemini_settings vs
validate_gemini_settings_strict) and unblocks legitimate configs such as
AWS Bedrock, Vertex AI, and custom Gemini base URLs that the UI previously
refused to save.

Refs: #2196, #1204

* fix(provider-form): address review feedback on soft-validation

P1: move empty providerKey back to hard rejection for OpenCode / OpenClaw /
Hermes. Since providerKey is the primary identity for these apps and the
mutations layer throws "Provider key is required" when absent, letting users
click "save anyway" would surface a generic error toast instead of a
precise, actionable one. Treat empty providerKey as an integrity constraint
alongside regex / duplicate checks.

P2: give the soft-confirm submit path its own submitting state. The
confirm-dialog path bypassed react-hook-form's isSubmitting lifecycle, so
slow or failing saves left the outer submit button responsive and could
spawn unhandled rejections. Now the confirm handler awaits performSubmit
inside try/catch/finally, uses an isConfirmSubmitting flag to gate both
confirm and cancel clicks, and folds the flag into the outer disabled
state and onSubmittingChange callback.

Refs: #2307 review comments

* chore(clippy): use push for single char '…' in truncate_body

Clippy 1.95 added single_char_add_str which flagged the push_str("…")
in truncate_body. Rebased onto latest upstream/main and applied the
suggested fix so the Backend Checks clippy job passes.

Unrelated to this PR's core changes; bundled in so the PR is mergeable
without waiting for a separate upstream fix.

---------

Co-authored-by: Allen <allen@AllenMacBook-M4-Pro.local>
2026-04-25 09:28:28 +08:00
Jason 67dbfc0a8c fix(model-fetch): support /models for Anthropic-compat subpath providers
Providers like DeepSeek, Kimi, Zhipu GLM and MiniMax expose the
Anthropic-compatible API on a subpath (e.g. /anthropic) while the
OpenAI-style /models endpoint lives at the API root. The previous
heuristic blindly appended /v1/models to the Base URL, so every such
provider returned 404 and the UI mislabeled it as "provider does not
support fetching models".

Backend now generates a candidate list and tries them in order:
preset override -> baseURL /v1/models -> stripped-subpath /v1/models ->
stripped-subpath /models. Non-404/405 responses (auth, network) stop
immediately so we never retry against hostile status codes. Known
compat suffixes are kept in a length-descending constant so the
longest match wins; response bodies are truncated to 512 chars to
avoid HTML 404 pages bloating the error string.

Preset type gains an optional modelsUrl (DeepSeek points at
https://api.deepseek.com/models). Frontend threads the override
through fetchModelsForConfig when the current Base URL still matches
the preset default. A new fetchModelsEndpointNotFound i18n key
replaces the misleading "not supported" toast for exhausted-candidate
and 404/405 cases (zh/en/ja).
2026-04-24 23:20:10 +08:00
Coconut-Fish 010b163430 Fix/一键配置失效 (#2249)
* style(FailoverQueueManager): 显示供应商备注信息

* style(FailoverQueueItem): 添加供应商备注字段以支持备注信息显示

* style(FailoverQueueManager): 显示供应商备注信息

* style(FailoverQueueItem): 添加供应商备注字段以支持备注信息显示

* style(FailoverQueueManager): 更新供应商备注信息的显示样式

* style(FailoverQueueItem): 添加条件序列化以优化供应商备注字段

* fix: 优化模型状态管理,确保配置更新时正确引用最新设置

* fix(skill): improve error handling for skill source directory resolution

Co-authored-by: Copilot <copilot@github.com>

* fix(gemini): simplify project directory retrieval in scan_sessions function

* fix(useModelState): optimize latestConfigRef assignment in useModelState hook

* fix(useModelState): remove unnecessary blank line in useModelState hook

---------

Co-authored-by: Copilot <copilot@github.com>
2026-04-23 15:36:03 +08:00
Jesus Díaz Rivas 10e0772d8c feat: Add Codex OAuth FAST mode toggle (#2210)
* Add Codex OAuth FAST mode toggle

* fix(codex-oauth): default FAST mode to off to avoid surprise quota burn

service_tier="priority" consumes ChatGPT subscription quota at a higher
rate. Users must now opt in explicitly rather than inherit FAST mode
silently when this feature ships.

---------

Co-authored-by: Jason <farion1231@gmail.com>
2026-04-23 12:05:17 +08:00
Jason db92f2b3b0 style(providers): apply prettier to common-config hooks
Reformat three hooks brought in from upstream #2191 so that
format:check passes in CI.
2026-04-21 12:05:48 +08:00
Jason 0be75668cc feat(hermes): align provider schema with Hermes Agent 0.10.0
Hermes 0.10.0 tightened custom_providers validation (commit 2cdae233):
invalid base_urls are rejected, unknown fields produce warnings, and
new fields (rate_limit_delay, bedrock_converse, key_env) landed.

- Add bedrock_converse to the api_mode selector (and i18n labels)
- Expose rate_limit_delay in a provider-level advanced panel
- Validate base_url client-side (URL shape, template-token friendly)
- Drop per-model max_tokens — not in _VALID_CUSTOM_PROVIDER_FIELDS
- Round-trip test asserts set_provider preserves rate_limit_delay /
  key_env / any unknown forward-compat field
2026-04-21 11:57:07 +08:00
Jason c8d91298c7 fix(providers): drop legacy ANTHROPIC_REASONING_MODEL from Claude quick-set
The model-mapping quick-set button referenced an undefined `reasoningModel`
prop and wrote `ANTHROPIC_REASONING_MODEL`, which the backend explicitly
marks as deprecated legacy (see services/provider/mod.rs and
services/proxy.rs). Remove all three references so typecheck passes and
the button matches the provider model schema.
2026-04-21 11:57:07 +08:00
Jason 21a1518f9f refactor(hermes): drop "Auto" api_mode and require explicit protocol
Hermes' built-in api_mode detection only matches a handful of official
endpoints (api.openai.com, api.anthropic.com, api.x.ai, AWS Bedrock);
third-party / proxy endpoints silently fall back to chat_completions,
which causes opaque 401/404s on Anthropic-protocol or Codex-Responses
providers. The "Auto" option was misleading for the common third-party
case.

- Drop the "Auto" option from the API Mode dropdown; remove the
  HermesApiModeChoice sentinel type so writes always emit api_mode.
- Default new providers and legacy entries lacking api_mode to
  chat_completions (only persisted on user save).
- Deeplink imports now write api_mode: chat_completions explicitly
  instead of relying on URL heuristics; test renamed accordingly.
- Rename the "Codex Responses (Copilot / OpenCode)" label to
  "OpenAI Responses" to match OpenAI's /v1/responses naming.
2026-04-21 11:57:07 +08:00
Jason f57edfd697 refactor(hermes): share provider-source marker constants and write guard
After /simplify review of the P1-3 second wave, two small cleanups:

- Lift the `_cc_source` / `providers_dict` magic strings out of
  ProviderCard into a shared helper (`isHermesReadOnlyProvider`) and
  named constants in hermesProviderPresets.ts. Front-end and back-end
  now document the same marker contract in two mirrored places
  instead of drifting strings.

- Replace the duplicate `is_dict_only_provider` + `format!` branches
  at the top of `set_provider` / `remove_provider` with a single
  `ensure_provider_writable(config, name, verb)` guard. Future error
  copy tweaks only have to happen once.

No behaviour change; all 52 hermes_config tests stay green.
2026-04-21 11:57:07 +08:00
Jason abb305a82f feat(hermes): render providers: dict overlays as read-only cards
ProviderCard now detects Hermes provider entries sourced from the
v12+ `providers:` dict via the `_cc_source` marker that the backend
injects, and renders a "Hermes Managed" badge beside the title.
ProviderActions receives an `isReadOnly` prop that disables the Edit
and Delete buttons (with a tooltip pointing the user at Hermes Web
UI) while keeping Switch and Duplicate enabled — switching only
touches `model.*`, and duplicate lets users fork the overlay into
their own `custom_providers:` list.

Three-locale i18n keys `provider.managedByHermes` /
`provider.managedByHermesHint` added.
2026-04-21 11:57:07 +08:00
Jason 828ec2ce07 fix(hermes): show active provider and wire add/enable/remove actions
Switching a Hermes provider previously only fired a toast because the frontend treated Hermes as non-additive (unlike backend AppType::is_additive_mode, which lists OpenCode | OpenClaw | Hermes) and relied on the unused is_current DB flag for highlighting. Align the UI model with the backend:

- Include Hermes in ProviderActions' isAdditiveMode so the main button switches between "Add" and "Remove".
- Drive the "current" highlight from model.provider (via useHermesModelConfig) instead of the DB is_current field; model.provider is Hermes's real SSOT for the active provider.
- Reuse OpenClaw's set-as-default button slot to expose an "Enable" action on Hermes that calls switchProvider, so providers already in config can be activated without re-adding. switch_normal + apply_switch_defaults already atomically update custom_providers and model.provider, so no backend change is needed.
- Invalidate liveProviderIds + modelConfig + health in parallel after add/update/delete/switch via a new invalidateHermesProviderCaches helper, replacing four copies of three sequential awaits.
2026-04-21 11:57:06 +08:00
Jason 497a543c1b feat(hermes): add API mode dropdown and per-provider model editor in form
The Hermes provider form previously only exposed Base URL and API Key,
forcing users to drop into the Model panel to hand-edit model IDs after
adding a provider. Following OpenClaw's shape, the form now carries:

- An API Mode selector (auto-detect / chat_completions / anthropic_messages).
  "auto" is a UI-only sentinel — selecting it deletes api_mode from the
  config so the YAML doesn't leak a redundant field.
- A model list editor where the first row is badged as the default and
  each row has a collapsible Advanced panel for context_length and
  max_tokens. Adding/removing rows uses a UUID-keyed ref so typing in
  one input doesn't drop focus when another row is added.
- A Fetch Models button that pulls /v1/models from the configured
  endpoint and exposes the catalog in a per-row dropdown, identical to
  OpenClaw's flow. The vendor grouping is memoized so keystrokes don't
  trigger a reduce+sort per model row — Radix DropdownMenuContent does
  not lazy-mount, so the inner JSX evaluates on every render regardless
  of whether the menu is open.

Three-locale i18n keys are added together (zh/en/ja).
2026-04-21 11:57:06 +08:00
Jason 8b65a31c7c feat(claude): upgrade effort toggle from "high" to "max"
Per Anthropic's effort parameter docs, "high" is the API default and
setting effortLevel="high" is equivalent to omitting the field entirely.
The toggle previously produced no effect.

Claude Opus 4.6, Sonnet 4.6, and Opus 4.7 now support a "max" level
that enables unconstrained reasoning. Rename the checkbox (effortHigh
-> effortMax) and write effortLevel="max" when toggled on. Existing
"high" values in user configs are left untouched.

Updates zh/en/ja locales and user-manual entries accordingly.
2026-04-21 11:57:06 +08:00
Jason 0ca36b9d51 fix: address Hermes review findings (5 medium issues)
- Add missing Hermes MCP import on first launch (lib.rs)
- Add Hermes branch in ProviderForm defaultValues fallback
- Include Hermes in session manager subtitle (zh/en/ja)
- Rename check_openclaw_stream to check_additive_app_stream
- Cache parsed HERMES_DEFAULT_CONFIG to avoid repeated JSON.parse
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 a0b585992a feat: add Hermes frontend types, API layer, and hooks (Phase 7)
- Add "hermes" to AppId union type and all exhaustive Record<AppId>
- Add HermesModelConfig, HermesAgentConfig, HermesEnvConfig types
- Add hermes field to VisibleApps, McpApps, ProxyTakeoverStatus
- Create src/lib/api/hermes.ts with Tauri invoke wrappers
- Create src/hooks/useHermes.ts with 5 query + 3 mutation hooks
- Register hermes in APP_IDS, APP_ICON_MAP (violet color scheme)
- Split MCP_SKILLS_APP_IDS into MCP_APP_IDS (includes hermes) and
  SKILLS_APP_IDS (excludes hermes, since Hermes has no Skills support)
- Wire hermes additive-mode into App.tsx (remove/duplicate handlers),
  ProviderList.tsx (live provider ID query + In Config badge),
  mutations.ts (cache invalidation on switch/add/delete)
- Add Hermes checkbox to McpFormModal
- Add basic hermes i18n keys (en/zh/ja)
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
zxZeng cc77a040e2 fix: common config checkbox state not persisting for Codex/Claude/Gemini (#2191)
修复:Codex/Claude/Gemini 通用配置勾选状态无法正确保存的问题

Co-authored-by: 曾兴 <zengx@gantsoftware.com>
2026-04-21 11:04:31 +08:00
King 8ba92b5470 feat(ui): add quick-set button for model mapping fields (#2179)
* feat(ui): add quick-set button for model mapping fields

Add a "一键设置" (Quick Set) button in the model mapping section to
simplify provider configuration. When users enter a model name in any
of the five model fields, they can now click this button to populate
all fields with that same value.

This addresses the UX friction of manually filling all five model
mapping fields (主模型, 推理模型, Haiku, Sonnet, Opus) when the
provider uses the same model name across all request types.

Implementation:
- Add Wand2 icon import from lucide-react
- Insert quick-set button alongside existing fetch-models button
- Logic picks first non-empty model value and applies to all fields
- Show success toast after applying
- Disabled state when all model fields are empty
- Add i18n strings for zh, en, ja locales

Relates to user feedback about tedious model configuration workflow.

* style(ui): format ClaudeFormFields component code

Apply consistent code formatting to ClaudeFormFields.tsx following
project linting rules. Includes multi-line import statements and
improved readability for conditional expressions.
2026-04-19 21:42:17 +08:00
hotelbe 87635e7fc6 feat(copilot): add GitHub Enterprise Server support (#2175)
* feat(copilot): add GitHub Enterprise Server support

* fix(copilot): address GHES PR review findings (P1 + 2×P2)

- P1: Use composite account ID (domain:user_id) for GHES to prevent
  cross-instance ID collisions; github.com keeps plain numeric ID for
  backward compatibilit
- P2-a: Use get_api_endpoint() for model list URL with automatic
  fallback to static URL when dynamic endpoint resolution fails
- P2-b: Add normalize_github_domain() as backend SSOT for domain
  normalization (lowercase, strip protocol/path/query, reject userinfo)

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 20:29:46 +08:00
Dex Miller 03a0f9661b feat(proxy): Gemini Native API proxy integration (#1918)
* refactor(proxy): extract take_sse_block helper with CRLF delimiter support

Replace inline `buffer.find("\n\n")` SSE splitting logic across streaming,
streaming_responses, response_handler, and response_processor with a shared
`take_sse_block` function that handles both `\n\n` and `\r\n\r\n` delimiters.

* feat(proxy): add Gemini Native URL builder and full-URL resolver

Introduce gemini_url module that normalizes legacy Gemini/OpenAI-compatible
base URLs into canonical models/*:generateContent endpoints. Supports both
structured Gemini URLs (auto-normalized) and opaque relay URLs (pass-through
with query params only).

* feat(proxy): add Gemini Native schema, shadow store, transform, and streaming

- gemini_schema: Gemini generateContent request/response type definitions
- gemini_shadow: session-scoped shadow store for thinking signature and
  tool-call state replay across streaming chunks
- transform_gemini: bidirectional Anthropic Messages ↔ Gemini Native
  request/response conversion with thinking block and tool-use support
- streaming_gemini: Gemini SSE → Anthropic SSE streaming adapter with
  incremental thinking/text/tool_use delta emission

* feat(proxy): wire Gemini Native format into proxy core and Claude adapter

Integrate gemini_native api_format throughout the proxy pipeline:
- ClaudeAdapter: detect Gemini provider type, Google/GoogleOAuth auth
  strategies, and suppress Anthropic-specific headers for Gemini targets
- Forwarder: Gemini URL resolution, shadow store threading, endpoint
  rewriting to models/*:generateContent with stream/non-stream variants
- Handlers: route Gemini streaming through streaming_gemini adapter and
  non-streaming through transform_gemini converter
- Server/State: add GeminiShadowStore to shared ProxyState
- StreamCheck: support gemini_native health check with proper auth headers

* feat(ui): add Gemini Native provider preset and api format option

- Add gemini_native to ClaudeApiFormat type and ProviderMeta.apiFormat
- Add "Gemini Native" provider preset with default Google AI endpoints
- Show Gemini-specific endpoint hints and full-URL mode guidance
- Add gemini_native option to API format selector in ClaudeFormFields
- Add i18n strings for zh/en/ja

* feat(proxy): add Gemini Native tool argument rectification

* feat(proxy): update Gemini streaming and transformation logic

* fix(proxy): align shadow turns to tail on client history truncation

* fix: revert unrelated cache_key change in claude proxy transform

Restore .unwrap_or(&provider.id) fallback for cache_key to match main
branch behavior. Only gemini_native related changes should be in this branch.

* Prevent Gemini review regressions in streaming and tool rectification

PR #1918 review feedback exposed two correctness issues in the Gemini Native adapter path. Gemini SSE buffering was still using lossy UTF-8 decoding, which could corrupt split multibyte payloads and drop streamed output. Tool arg rectification also removed top-level parameters eagerly, which broke tools that legitimately define a parameters field.

This change moves Gemini SSE buffering onto the existing append_utf8_safe path and makes parameters flattening conditional on the schema actually expecting nested extraction. The old Skill rectification path stays intact, and new regression tests cover both the preserved parameters case and UTF-8-split JSON payloads.

Constraint: Existing PR #1918 review feedback must be fixed without staging unrelated local docs and artifact files
Rejected: Keep String::from_utf8_lossy in Gemini SSE buffering | corrupts split multibyte payloads and can drop JSON chunks
Rejected: Always preserve the parameters wrapper | regresses the existing nested-parameters rectification path for Skill-style tools
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep Gemini SSE buffering on the UTF-8-safe accumulator path and only unwrap parameters when the target schema does not declare it as a legitimate field
Tested: cargo fmt --manifest-path src-tauri/Cargo.toml --all; cargo test --manifest-path src-tauri/Cargo.toml preserves_utf8_boundaries_when_json_payload_spans_chunks; cargo test --manifest-path src-tauri/Cargo.toml gemini_to_anthropic_rectifies_tool_args_from_schema_hints; cargo test --manifest-path src-tauri/Cargo.toml rectifies_streamed_skill_args_from_nested_parameters; cargo test --manifest-path src-tauri/Cargo.toml gemini_to_anthropic_preserves_legitimate_parameters_arg
Not-tested: Full src-tauri test suite; live end-to-end Gemini relay traffic against upstream services

* Keep Gemini tool replay stable across Claude request boundaries

Claude Code follow-up requests were still falling back to locally reconstructed functionCall parts, which dropped Gemini thought signatures and triggered INVALID_ARGUMENT errors from the official Gemini API. The replay path needed to survive real Claude request boundaries, not just idealized in-process test flows.

This change makes Claude requests reuse X-Claude-Code-Session-Id as the shadow session key, records streamed Gemini tool turns before tool_use events are fully drained, and matches assistant tool_use turns to shadow state by tool_use id and normalized tool name before positional fallback. Together these fixes keep thoughtSignature-bearing Gemini tool calls available for the next request in the loop.

Constraint: Claude Code sends a stable X-Claude-Code-Session-Id header while metadata.session_id may be absent on follow-up requests
Rejected: Rely on metadata-only Claude session extraction | generated fresh session ids and broke cross-request shadow replay
Rejected: Record Gemini shadow only after streaming completes | loses the race when the client sends the next request immediately after tool_use
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Preserve Gemini shadow continuity across requests by keying Claude sessions from the header first and persisting tool-call shadow before yielding tool_use events downstream
Tested: cargo fmt --manifest-path src-tauri/Cargo.toml --all; cargo test --manifest-path src-tauri/Cargo.toml test_extract_session_from_claude_header; cargo test --manifest-path src-tauri/Cargo.toml test_extract_session_from_claude_header_precedes_metadata; cargo test --manifest-path src-tauri/Cargo.toml stores_tool_shadow_before_tool_use_events_are_fully_drained; cargo test --manifest-path src-tauri/Cargo.toml shadow_replay_matches_tool_use_turn_by_id_when_position_drifts; cargo test --manifest-path src-tauri/Cargo.toml shadow_replay_aligns_to_latest_turns_after_client_truncation
Not-tested: Full src-tauri test suite without test filters; live end-to-end Gemini relay after this exact commit hash

* style: apply cargo fmt to pass Backend Checks CI

Wrap prompt_cache_key chained call across lines per rustfmt default
formatting. Pure formatting change, no behavior difference.

* fix(proxy/gemini): synthesize unique ids for no-id tool calls + enforce object params schema

P1 — Parallel tool calls without Gemini-assigned ids no longer collapse.
Gemini 2.x native parallel `functionCall` entries may omit the `id` field.
The previous `merge_tool_call_snapshots` fell back to matching by `name`,
which silently merged two parallel calls to the same function into one
entry — dropping the first call's args. The non-streaming path and shadow
store further bottlenecked on empty-string ids: multiple `tool_use` blocks
shared the same id, and `tool_name_by_id.get("")` could only return one
mapping, causing later `tool_result` round-trips to fail with
`Unable to resolve Gemini functionResponse.name` or bind to the wrong tool.

Fix: introduce `synthesize_tool_call_id()` producing `gemini_synth_<uuid>`.
Both streaming and non-streaming response paths now guarantee every
Anthropic-visible tool_use carries a unique id. `merge_tool_call_snapshots`
matches by id first, falling back to the `parts` array position (for the
cumulative-streaming case) while preserving the synthesized id across
chunks. `convert_message_content_to_parts` detects the synthetic prefix
and strips the id from outbound `functionCall`/`functionResponse` so the
internal identifier never leaks upstream. `shadow_parts` performs the
same strip when replaying a recorded assistant turn.

P2 — Vertex AI rejects empty `parameters` schemas. When an Anthropic tool
arrives with missing or empty `input_schema`, the proxy used to emit
`"parameters": {}` (no `type`), which fails Vertex AI validation with
`functionDeclaration parameters schema should be of type OBJECT`.
Contrary to the automated-review suggestion, the fix is not to omit
`parameters` (that too is rejected) but to normalize to the canonical
empty-object form `{type: "object", properties: {}}`.
Refs: google-gemini/generative-ai-python#423, BerriAI/litellm#5055.

Fix: new `ensure_object_schema` helper in `gemini_schema` promotes
missing `type` to `"object"` and adds empty `properties` when absent,
while leaving atomic (non-object) schemas untouched.

Tests: seven new regressions covering parallel no-id calls, cumulative
chunk id reuse, synthetic-id round-trip both directions, shadow replay
id stripping, and the three Vertex-AI schema shapes.

The two existing wrapper functions (`gemini_to_anthropic` and
`gemini_to_anthropic_with_shadow`) gain `#[allow(dead_code)]` to clear
a pre-existing clippy -D warnings failure — they are part of the public
transform API surface and intentionally kept for future callers.

Addresses Codex review P1/P2 on #1918.

* fix(proxy/gemini): narrow URL normalization + guard empty OAuth access_token

P2a — Preserve opaque relay URLs that contain `/v1/models/` prefixes.

`should_normalize_gemini_full_url` previously flagged any full URL whose
path merely contained `/v1beta/models/` or `/v1/models/` as a structured
Gemini endpoint, forcing rewrite to `.../v1beta/models/{model}:method`.
This silently dropped legitimate relay route segments (e.g.
`https://relay.example/v1/models/invoke` → `.../v1beta/models/...:generateContent`,
losing `/invoke`) and sent traffic to the wrong upstream path.

Replace the bare `contains(...)` checks with
`matches_structured_gemini_models_path`, which requires the
`/models/` segment to be followed by a canonical Gemini method call
(`*:generateContent` or `*:streamGenerateContent`). The
`matches_bare_gemini_models_path` helper is generalized (and renamed) to
handle both `/v1beta/models/` and `/v1/models/` alongside the original
bare `/models/` shape.

P2b — Reject empty Gemini OAuth access_tokens before they reach the
bearer header.

`GeminiAdapter::parse_oauth_credentials` accepts refresh-token-only JSON
(and surfaces `{"access_token": "", ...}` for expired credentials) with
`access_token` defaulting to `""`. The Claude adapter's GeminiCli branch
then called `AuthInfo::with_access_token(key, creds.access_token)`
unconditionally, so the bearer-header builder at
`AuthStrategy::GoogleOAuth` resolved to `Authorization: Bearer ` — a
deterministic 401 from upstream.

CC Switch does not currently exchange the refresh_token for a fresh
access_token (`OAuthCredentials::needs_refresh` / `can_refresh` are
annotated `#[allow(dead_code)]`). Until that exists, only attach
`access_token` when it is non-empty; fall back to plain GoogleOAuth
strategy with the raw key and log a warn pointing users at
`~/.gemini/oauth_creds.json` so the failure mode is observable.

Tests:
- gemini_url.rs: three new regressions — opaque `/v1/models/invoke`,
  opaque `/v1beta/models/route`, and the positive counter-case where a
  structured `/v1/models/...:generateContent` path still normalizes.
- claude.rs: three new `test_extract_auth_gemini_cli_*` tests covering
  refresh-only JSON, empty-string access_token JSON, and the valid-JSON
  pass-through.

All 839 lib tests pass; cargo fmt + clippy -D warnings clean.

Addresses Codex review P2 findings on #1918.

* fix(proxy/gemini): treat empty-string functionCall id as missing in streaming path

Follow-up to the earlier P1 fix: some Gemini relays serialize an absent
functionCall id as `"id": ""` instead of omitting the field. The
non-streaming `extract_tool_call_meta` already filters these via
`.filter(|s| !s.is_empty())`, but the streaming counterpart
`extract_tool_calls` passed the empty string straight through
`function_call.get("id").and_then(|v| v.as_str())` into
`GeminiToolCallMeta::new`, producing a `Some("")` id.

Downstream, `merge_tool_call_snapshots` would then match two parallel
no-id calls against each other on their shared empty-string id,
collapsing them into a single snapshot (silent data loss for the first
call) and emitting an Anthropic `tool_use.id: ""` that breaks tool_result
correlation on the Claude Code client.

Fix:
- `extract_tool_calls`: apply the same `filter(|s| !s.is_empty())` guard
  used in the non-streaming path so empty strings become `None` before
  reaching the shadow meta.
- `merge_tool_call_snapshots`: defensively collapse any incoming
  `Some("")` to `None` up front — keeps the "missing vs present" invariant
  local to the merge step for future callers that might build
  `GeminiToolCallMeta` by hand.

Tests (2 new, both in streaming_gemini):
- `parallel_empty_string_id_calls_are_treated_as_missing_and_preserved`
  covers two parallel calls with explicit `"id": ""` — asserts both
  surface, no empty tool_use id leaks, and each gets a unique
  `gemini_synth_` id.
- `single_empty_string_id_tool_call_gets_synthesized_id` covers the
  non-parallel degraded-relay case.

All 841 lib tests pass; cargo fmt + clippy -D warnings clean.

Addresses Codex follow-up P1 on #1918.

* fix(proxy/gemini): gate generic REST path suffixes behind Google host whitelist

`should_normalize_gemini_full_url` previously treated any full URL whose
path ends with `/v1`, `/v1/models`, `/models`, `/v1/openai`, or `/openai`
as a structured Gemini endpoint and rewrote it to
`/v1beta/models/{model}:generateContent`. These are ubiquitous REST
conventions — opaque relays such as `https://relay.example/custom/v1`
legitimately use them for fixed endpoints — so the rewrite silently
routed traffic to the wrong upstream path.

Split the predicate into two layers:

- **Unconditional**: `matches_structured_gemini_models_path` (i.e. a
  `/models/...:generateContent` method call anywhere in the path), the
  Google-specific `/v1beta*` family, and the deep OpenAI-compat paths
  (`/v1beta/openai/chat/completions`, `/openai/chat/completions`, and
  their `responses` siblings). These remain host-agnostic because the
  path grammar itself is Gemini-specific.
- **Google-host gated**: `/v1`, `/v1/models`, `/models`, `/v1/openai`,
  `/openai`. Only normalized when the host is one of
  `generativelanguage.googleapis.com`, `aiplatform.googleapis.com`, or a
  real `*-aiplatform.googleapis.com` Vertex regional endpoint. The match
  is exact/suffix (not `contains`), so lookalike hosts like
  `aiplatform.example.com` are correctly treated as opaque relays.

Tests (8 new in `gemini_url::tests`):
- Four opaque-relay cases: `/custom/v1`, `/custom/models`,
  `/custom/v1/models`, `/custom/openai` — all preserved as-is.
- Three Google-host counter-cases: `/v1`, `/models`, and
  `us-central1-aiplatform.googleapis.com/v1` still normalize.
- One lookalike safety case: `aiplatform.example.com/v1` is NOT
  treated as Google.

All 849 lib tests pass; cargo fmt + clippy -D warnings clean.

Addresses Codex review P2 on #1918.

* fix(proxy/gemini): align shadow id with client-visible id in non-streaming path

When Gemini returns a `functionCall` without an id (common in 2.x
parallel calls), `gemini_to_anthropic_with_shadow_and_hints` previously
generated TWO independent synthesized UUIDs:

  1. Line 186-197 — synthesized id `A` used for the Anthropic-visible
     `content[tool_use].id` returned to the client.
  2. Line 850-881 — `extract_tool_call_meta` independently synthesized
     id `B ≠ A`, which populated `shadow_turn.tool_calls[i].id`.

`shadow_content` (line 225-228, cloned from `rectified_parts`) retained
the original missing/empty id. Result: the client sees id `A`, the
shadow store holds id `B`.

On the next turn, `convert_messages_to_contents` builds
`tool_name_by_id` from `build_tool_name_map_from_shadow_turns`, which
uses `tool_calls[i].id` — so the map contains `B → name` but not
`A → name`. When the client sends back `tool_result(tool_use_id=A)`,
resolution fails with:

  Unable to resolve Gemini functionResponse.name for tool_use_id `A`

This affects both truncated histories (client sends only the
tool_result) and full histories (shadow-replay branch at line 342-354
skips `convert_message_content_to_parts`, so the assistant tool_use
block never registers id `A` itself).

Fix: make `rectified_parts` the single source of truth. After
`rectify_tool_call_parts`, run a pre-pass that writes
`synthesize_tool_call_id()` back into any `functionCall` that lacks a
non-empty id. All three readers — the content builder (186-197), the
shadow_content clone (225-228), and `extract_tool_call_meta` — then
observe the same id. `shadow_parts()` already strips synthesized ids on
replay (line 616-628), so the internal identifier never leaks to
Gemini upstream.

This mirrors the streaming path, which already has single-source-of-
truth semantics via `tool_call_snapshots` in `streaming_gemini.rs` —
no change needed there.

Tests (5 new in `transform_gemini::tests`):
- `non_stream_shadow_id_matches_client_visible_id`: asserts
  `response.content[0].id == shadow.tool_calls[0].id ==
  shadow.assistant_content.parts[0].functionCall.id`.
- `non_stream_missing_id_scenario_a_truncated_history_resolves`: turn 2
  sends only `[tool_result(id=A)]`; resolution must succeed.
- `non_stream_missing_id_scenario_b_full_history_replay_resolves`: turn 2
  sends `[assistant(tool_use=A), tool_result(A)]`; shadow-replay branch
  strips the synth id from outgoing `functionCall` while still
  resolving the subsequent `tool_result`.
- `non_stream_preserves_original_gemini_id_when_present`: regression —
  genuine Gemini ids flow through unchanged.
- `non_stream_synthesized_id_not_leaked_to_gemini_via_shadow_replay`:
  defensive — shadow-replay path must strip synth ids from both
  `functionCall.id` and `functionResponse.id`.

All 854 lib tests pass; cargo fmt + clippy -D warnings clean.

Addresses Codex follow-up P1 on #1918.

* refactor(proxy/gemini): share build_anthropic_usage between stream and non-stream paths

`streaming_gemini::anthropic_usage_from_gemini` and
`transform_gemini::build_anthropic_usage` were byte-for-byte identical
(32 lines each) — both converting Gemini `usageMetadata` into the
Anthropic `usage` shape including `cache_read_input_tokens` mapping.

Promote the non-streaming version to `pub(crate)` and reuse it from the
streaming SSE converter. Removes ~30 lines of duplication and guarantees
the two paths cannot drift apart.

No behavioral change; all 854 lib tests pass; cargo fmt + clippy -D
warnings clean.

* fix(proxy/gemini): gate /v1beta behind Google host + normalize models/ model id prefix

Two related P2 corrections to the Gemini Native URL surface, both
folding into the existing Google-host-whitelist architecture.

## P2a — `/v1beta` suffix should not unconditionally trigger rewrite

`should_normalize_gemini_full_url` placed `/v1beta` and `/v1beta/models`
in the unconditional layer on the reasoning that `/v1beta` is
Google-specific. In practice an opaque relay fronting a non-Gemini
service at `https://relay.example/custom/v1beta` would still be
silently rewritten to `/v1beta/models/{model}:generateContent`,
breaking the deployment.

Move `/v1beta`, `/v1beta/models`, and `/v1beta/openai` into the
Google-host gated layer alongside `/v1`, `/models`, and friends. The
unconditional layer now only accepts paths whose grammar is
intrinsically Gemini — `/models/...:generateContent` method calls and
the deep OpenAI-compat endpoints like `/openai/chat/completions` and
`/openai/responses`. Pasted AI-Studio URLs such as
`https://generativelanguage.googleapis.com/v1beta` still normalize
because the host matches the whitelist.

## P2b — `model: "models/gemini-2.5-pro"` produced doubled path prefix

Gemini SDKs (and the official `list_models` response) commonly surface
model ids in resource-name form `models/gemini-2.5-pro`. Raw
interpolation into `format!("/v1beta/models/{model}:...")` produced
`/v1beta/models/models/gemini-2.5-pro:streamGenerateContent` which
upstream rejects — yielding false-negative health checks for otherwise
valid provider configs.

Introduce `normalize_gemini_model_id(&str) -> &str` in `gemini_url`
as the single source of truth: strips an optional leading `/` then an
optional `models/` prefix, leaving bare ids untouched. Apply in the
three call sites that build a Gemini method URL:
- `services/stream_check.rs::resolve_claude_stream_url` (unified path)
- `services/stream_check.rs::check_gemini_stream` (Gemini-only path)
- `proxy/forwarder.rs::rewrite_claude_transform_endpoint` (production)

Tests (9 new):
- `gemini_url`: 3 regressions for opaque vs Google-host `/v1beta*`
  handling + 5 unit tests pinning `normalize_gemini_model_id` behavior
  (strip prefix, leave bare id, preserve nested slashes past the one
  stripped prefix, tolerate leading slash, pass through empty input).
- `stream_check`: one end-to-end regression confirming
  `models/gemini-2.5-pro` collapses to the expected single-prefix URL.
- `forwarder`: one end-to-end regression on the production rewrite
  path.

All 864 lib tests pass; cargo fmt + clippy -D warnings clean.

Addresses Codex P2 feedback on #1918.

* fix(proxy/gemini): trim API key before provider-type detection and OAuth parsing

Leading whitespace on a copied oauth_creds.json (e.g. trailing newline
when the user copies the file content as-is) would slip past the
`starts_with("ya29.") || starts_with('{')` prefix check in
`ClaudeAdapter::provider_type`, causing the provider to be misclassified
as raw-API-key Gemini and fall back to `x-goog-api-key` with the raw
JSON as the key — which upstream rejects with 401.

The frontend's `handleApiKeyChange` already trims on keystrokes but
deep-link imports, the JSON editor, and live-config backfill all bypass
that path. Trim at every backend extraction point so the coverage is
uniform:

- `ClaudeAdapter::extract_key` (5 env / fallback branches) gets
  `.map(str::trim)` before `.filter(|s| !s.is_empty())` so that
  whitespace-only values are also treated as missing.
- `GeminiAdapter::extract_key_raw` gets the same chain (including
  the `.filter` it was missing before).
- `GeminiAdapter::parse_oauth_credentials` gets a defensive
  `let key = key.trim();` at the entry as a belt-and-suspenders guard.

Adds two regression tests covering JSON and bare `ya29.` keys with
leading newline/space.

* fix(proxy/gemini): gate generic REST suffix stripping behind Google host in non-full-URL mode

`build_gemini_native_url` unconditionally stripped `/v1`, `/v1beta`,
`/models`, and `/openai` suffixes from the base path regardless of
host. This worked for Google's own endpoints but silently rewrote
third-party relay URLs like `https://relay.example/custom/v1` to
`.../custom/v1beta/models/...`, breaking any relay that mounts its
Gemini-compatible namespace under a versioned prefix.

The result was also asymmetric with the previously-fixed full-URL
branch: toggling the "full URL" switch changed the outbound URL for
the same base_url, which is exactly the kind of invisible behavior
that makes debugging proxy deployments painful.

Align `normalize_gemini_base_path` with
`should_normalize_gemini_full_url`'s layered model:

- Unconditional: `/models/...:method` structured paths and deep
  OpenAI-compat endpoints (`/openai/chat/completions`,
  `/openai/responses` and their versioned variants) — these are
  unambiguous Gemini-specific grammar on any host.
- Google-host gated: generic `/v1`, `/v1beta`, `/models`, `/openai`
  suffixes only get stripped on `generativelanguage.googleapis.com`,
  `aiplatform.googleapis.com`, or `*-aiplatform.googleapis.com`.
  Other hosts preserve the prefix verbatim so relays keep their
  intended routing.

Adds seven regression tests for the non-full-URL flow: opaque relay
preservation (v1 / v1beta / models / openai suffix variants), Google
host normalization (counter-case), and boundary cases (structured
method path and deep OpenAI-compat endpoint stripped regardless of
host).

Test count: 864 -> 873.

* Revert "fix(proxy/gemini): gate generic REST suffix stripping behind Google host in non-full-URL mode"

This reverts commit d19ff09cb7.

* test(proxy/gemini): pin non-full-URL versioned relay base stripping

Adds two regression tests that lock in the intentional asymmetry
between full-URL and non-full-URL modes:

- Full-URL mode: opaque base path (e.g. `https://relay.example/custom/v1beta`)
  is preserved verbatim. Already covered by
  `preserves_opaque_full_url_with_bare_v1beta_suffix`.
- Non-full-URL mode: base path MUST strip `/v1`, `/v1beta`, etc. so the
  standard `/v1beta/models/{model}:method` endpoint can be appended
  without producing a doubled `/v1beta/v1beta/models/...` path.

The non-full-URL contract is "base URL + cc-switch appends the
canonical Gemini endpoint". A user who needs a relay's custom
namespace (e.g. `/v1/models/...`) must use full-URL mode and paste
the complete method path. This commit adds regression coverage so a
future attempt to mirror full-URL's host-whitelist gating into
`normalize_gemini_base_path` will fail the test suite immediately.

* chore(lint): address clippy 1.95 findings in existing modules

CI upgraded to Rust 1.95 and flagged ten pre-existing warnings that
older toolchains did not enforce. None relate to the Gemini proxy
integration PR itself but they block CI on the feature branch, so
clean them up here as a separate commit for easy review:

collapsible_match:
- proxy/providers/gemini_schema.rs: `"items" if value.is_object()`
  match guard instead of nested if.
- proxy/providers/transform_responses.rs: fold
  `map_responses_stop_reason`'s `"completed"` / `"incomplete"` arms
  into match guards, relying on the existing `_ => "end_turn"` fall-
  through for non-matching guard conditions (semantics preserved).
- services/session_usage_codex.rs: fold
  `"session_meta" if state.session_id.is_none()` guard, relying on
  the existing `_ => {}` fall-through.

unnecessary_sort_by:
- services/provider/endpoints.rs: `sort_by_key(|ep| Reverse(ep.added_at))`.
- services/skill.rs (backup list): same Reverse idiom on `created_at`.
- services/skill.rs (skill listings x2): `sort_by_key(|s| s.name.to_lowercase())`.

useless_conversion:
- services/skill.rs: drop the explicit `.into_iter()` on `zip`'s argument.

while_let_loop:
- services/webdav_auto_sync.rs: `while let Some(wait_for) = ...`
  instead of `loop { let Some(...) = ... else { break }; ... }`.

All changes are mechanical and preserve behavior. `cargo test --lib`
remains green (868 passed).

* fix(proxy/gemini): reconcile synthesized tool-call ids with later real ids + preserve thoughtSignature

Three related findings on `streaming_gemini.rs` for Gemini's cumulative
`streamGenerateContent` stream, all centered on `merge_tool_call_snapshots`:

1. (P1) Match upgraded tool-call IDs by position.
   When Gemini delivers a `functionCall` without an id on chunk 1
   (cc-switch synthesizes `gemini_synth_*`) and then upgrades it to a
   real id on chunk 2, the `Some(incoming_id)` branch only matched by
   id and missed the existing synthesized snapshot. A second entry
   would be pushed, yielding duplicate `tool_use` content blocks at
   stream end — one with the synthesized id, one with the real id —
   which could trigger duplicate tool execution and break tool_result
   correlation. Add a positional fallback: when no id match exists but
   the same-position slot holds a synthesized id, merge into it.
   `or(preserved_id)` already lets the real id win the merge.

2. (P2) Preserve prior thoughtSignature when merging snapshots.
   `tool_call_snapshots[index] = tool_call` overwrote the slot
   entirely, dropping any `thoughtSignature` captured on an earlier
   chunk if the current cumulative snapshot omitted it. Since
   `build_shadow_assistant_parts` writes `thoughtSignature` into the
   shadow turn from `tool_call.thought_signature`, a dropped signature
   would cause later replay requests to Gemini to be rejected with
   invalid-signature errors. Preserve the existing signature when the
   incoming chunk does not carry one.

3. (P2) Document the part-order streaming trade-off.
   All `tool_use` content blocks are emitted after the final text
   `content_block_stop`, so interleaved [text, functionCall, text,
   functionCall] parts arrive at the Anthropic client as [text(concat),
   tool_use, tool_use] — different from the non-streaming transformer,
   which preserves part order. This is intentional given the cumulative
   snapshot model and the consumers we target (claude-code-like clients
   don't depend on strict interleaving for tool execution correctness).
   Add a block comment at the flush site describing the trade-off and
   what a strict-order fix would entail, so this isn't rediscovered as
   a bug later.

Regression tests:
- upgraded_real_id_merges_into_existing_synthesized_snapshot
- thought_signature_preserved_when_later_chunk_omits_it

Test count: 868 -> 870. clippy 1.95 clean. fmt clean.

* fix(proxy/gemini): prefer exact tool-call id over normalized-name fallback

The shadow-turn matcher used a three-branch `||` chain (id / full name /
normalized name). When two tools share a suffix (e.g. `server_a:search`
and `server_b:search`), the normalized-name clause could short-circuit
on an earlier turn whose id is actually wrong for the incoming tool_use,
mis-routing replay state (functionCall id / thoughtSignature) for later
tool_result resolution.

Split matching into two layers: when the incoming message carries any
tool_use ids, run id-based lookup first and return on the earliest hit.
Only fall back to full-name / normalized-name matching when the incoming
ids are absent or none of them resolve.

Add two regressions:

- shadow_replay_prefers_exact_id_match_over_normalized_name_collision
  Two shadow turns with colliding normalized names and two assistant
  messages whose ids cross the positional order; asserts each message
  replays the id-correct shadow turn (including thoughtSignature).

- shadow_replay_falls_back_to_name_when_ids_absent
  Shadow turn with no id and incoming tool_use with an empty id;
  asserts the name fallback still populates the replayed part.

---------

Co-authored-by: Jason <farion1231@gmail.com>
2026-04-16 22:42:49 +08:00
Jason 689ca08409 feat: classify stream check errors with color-coded toasts
Distinguish between "provider rejects probe" (yellow warning) and
"genuinely broken" (red error) in health check results.

Backend: add AppError::HttpStatus variant to carry structured HTTP
status codes, populate http_status on error results, classify codes
into short labels (e.g. "Auth rejected (401)"), and truncate overly
long response bodies.

Frontend: route 401/403/400/429/5xx to toast.warning with localized
hints explaining the error may not indicate actual unusability; route
404/402/connection errors to toast.error. Add i18n keys for all three
locales (zh/en/ja).

Also deduplicate check_once by reusing build_stream_check_result.
2026-04-14 17:11:13 +08:00
Jason 4a0b5c3dec refactor: remove per-provider proxy config feature
The per-provider proxy configuration (meta.proxyConfig) is removed
because its scope is too narrow and covered by global proxy settings
and proxy takeover mode. Users can achieve the same result via the
global proxy panel.

Changes:
- Remove ProviderProxyConfig type (frontend TS + backend Rust)
- Remove ProviderAdvancedConfig proxy UI block, keep testConfig/pricingConfig
- Simplify http_client: delete build_proxy_url_from_config,
  build_client_for_provider, get_for_provider
- Simplify forwarder/stream_check/model_fetch to use global client
- Remove i18n keys (en/zh/ja)
- Fix pre-existing test bug in transform.rs (extra None arg)
2026-04-14 14:26:55 +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
Dex Miller 3aef5217cb Restore first-class OMO Slim council support (#1981) (#1982)
cc-switch could already persist arbitrary OMO Slim agent keys and top-level fields, but the built-in metadata and UI copy still reflected the pre-council agent set. This made the upstream council feature look unsupported and pushed users toward manual JSON-only setup.

Promote council to a built-in OMO Slim agent, add copy that points top-level plugin settings at Other Fields, and lock the behavior with regression tests.

Constraint: oh-my-opencode-slim exposes council through both agents.council and top-level council config
Rejected: Add a dedicated council editor UI now | too much surface area for issue #1981
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep OMO Slim built-in agent metadata aligned with upstream agent additions before shipping UI support
Tested: pnpm exec vitest run tests/utils/omoConfig.test.ts tests/components/OmoFormFields.mergeCustomModelsIntoStore.test.ts
Tested: pnpm typecheck
Not-tested: End-to-end validation against a live oh-my-opencode-slim installation
Related: farion1231/cc-switch#1981
2026-04-10 22:36:32 +08:00
Dex Miller 8c32610a7d Align Thinking fallback with main-model-only Claude mappings (#1984)
The Claude provider form reopened with an empty Thinking model after users saved only a main model. This updates model-state hydration to mirror the existing Haiku-style fallback semantics: read ANTHROPIC_REASONING_MODEL when present, otherwise display ANTHROPIC_MODEL, without writing a synthetic reasoning field back into config.

Constraint: Existing Haiku, Sonnet, and Opus selectors already rely on read-time fallback behavior
Rejected: Persist ANTHROPIC_REASONING_MODEL from ANTHROPIC_MODEL automatically | would diverge from Haiku behavior and silently rewrite saved config
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep Thinking fallback read-only unless all model-mapping fields are intentionally migrated to write-through semantics
Tested: pnpm typecheck
Tested: pnpm test:unit (1 unrelated pre-existing failure in tests/components/UnifiedSkillsPanel.test.tsx mock setup)
Not-tested: Manual add-provider reopen flow in the desktop UI
2026-04-10 22:34:18 +08:00
Jason a5a5207f74 style: fix prettier formatting in ProviderCard and ProviderForm 2026-04-09 21:13:32 +08:00
Jason fa4297f4d3 feat(common-config): show first-run notice dialog when editing providers
Display a one-time informational dialog explaining the Common Config
Snippet feature when users first open the add/edit provider form.
Uses a derived isOpen state from settings to avoid race conditions.
Adds commonConfigConfirmed flag to both TS and Rust settings types.
2026-04-09 16:49:14 +08:00
Jason ecb1fed5db feat(common-config): add guide info and empty state to common config editor
Add an informational alert block at the top of the common config snippet
editor modal (Claude/Codex/Gemini) explaining what the feature is, why
it exists, and how to use it. Also add an empty state prompt when no
snippet has been extracted yet, guiding users to click "Extract from
Editor". Includes i18n support for zh/en/ja.
2026-04-09 16:49:14 +08:00
Jason 521571c4ca fix(usage): only show CLI subscription quota for active provider
CLI-credential-based subscriptions (Claude/Codex/Gemini) read from a
single global credential file, so the quota always reflects the last
CLI login rather than a specific provider. Showing it on non-current
cards is misleading when multiple official subscriptions exist.

Apply the same isCurrent + autoQuery pattern already used by Copilot
and Codex OAuth: only query and render the quota footer when the
provider is the currently active one.
2026-04-09 16:49:14 +08:00
Jason df5cb6d771 fix(usage): only auto-poll Copilot/ChatGPT quota for current provider
CopilotQuotaFooter and CodexOauthQuotaFooter called their hooks with a
hardcoded `enabled: true` plus an unconditional 5-minute refetch and
refetchOnWindowFocus, so non-current reverse-proxy cards kept polling
in violation of the project's "only the active provider auto-queries
on cooldown" rule. With multiple Copilot or ChatGPT accounts bound to
different cards, every card kept hitting its own usage endpoint.

Adopt the same pattern as useUsageQuery: keep `enabled` independent of
isCurrent so first-fetch and manual refresh still work, but gate
refetchInterval / refetchIntervalInBackground / refetchOnWindowFocus on
a new `autoQuery` option, and thread `isCurrent` from ProviderCard
through the footers into the hooks.
2026-04-09 16:49:14 +08:00