mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-27 16:26:16 +08:00
518d945eb85e9ac5cb7950e3e41319f8c2da93f3
724 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
518d945eb8 |
chore(usage): drop Hermes Agent tracking integration
Hermes aggregates all in-process API calls into a single sessions row with the `model` field locked to the initial model, so the usage dashboard cannot cleanly surface per-call billing context. Two rounds of UI workarounds (raw mapping, then `<model> @ <host>` display) did not resolve the user-facing confusion, so the whole tracking integration is dropped for now. Removes session_usage_hermes service (and its 17 tests), sync wiring in commands/usage.rs and lib.rs, _hermes_session/hermes_session entries in usage_stats SQL (provider_name_coalesce CASE and effective_usage_log_filter IN clause), frontend Tab/banner/dropdown/ icon entries, and four i18n keys per locale. Hermes app integration outside usage tracking (proxy routing, session manager, config) is preserved. Pre-existing hermes rows in proxy_request_logs are left as orphans — filtered out by the updated SQL and never surfaced in the UI. |
||
|
|
064b339bab | fix(claude): persist max effort via env (#2493) | ||
|
|
608ee35ecd |
feat: support launch warp and execute session (#2466)
* feat: support launch warp and execute session Signed-off-by: tison <wander4096@gmail.com> * other wires Signed-off-by: tison <wander4096@gmail.com> * for launch with provider Signed-off-by: tison <wander4096@gmail.com> * fixup indirection Signed-off-by: tison <wander4096@gmail.com> * clippy Signed-off-by: tison <wander4096@gmail.com> * address comments Signed-off-by: tison <wander4096@gmail.com> --------- Signed-off-by: tison <wander4096@gmail.com> |
||
|
|
f061b777b7 |
feat(usage): add Hermes Agent tracking + fix zero-cost bug + perf
Hermes: - Parse ~/.hermes/state.db sessions (incl. profiles/*/state.db) into proxy_request_logs with data_source='hermes_session', WAL-aware incremental sync, Hermes-reported cost preferred over model_pricing fallback Zero-cost bug (dashboard showed \$0 totals): - GPT-5.5 family default pricing (~83% of affected rows used GPT-5.5) - find_model_pricing_row: ASCII-lowercase normalization so "OpenAI/GPT-5.5@HIGH" matches seeded "gpt-5.5" - Startup cost backfill in async task: scan rows where total_cost <= 0 but tokens > 0, recompute via model_pricing in a single transaction Performance: - Add (app_type, created_at DESC) covering index for dashboard range queries - Add expression index on COALESCE(data_source, 'proxy') so dedup EXISTS subqueries use index lookup instead of full scan; drop superseded idx_request_logs_dedup_lookup Refactor: - row_to_request_log_detail helper (3-way de-dup; fixes cost_multiplier \"1\" vs \"1.0\" drift between callers) - Promote get_sync_state/update_sync_state to shared session_usage module (4 copies -> 1) - run_step helper in lib.rs replaces 9 if-let-Err blocks - maybe_backfill_log_costs returns bool to skip duplicate total_cost parsing in caller |
||
|
|
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. |
||
|
|
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> |
||
|
|
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). |
||
|
|
4737158439 |
feat(tray): show coding-plan usage for Kimi / Zhipu / MiniMax
|
||
|
|
cdcc423122 |
refactor(hermes): drop config health check scanner
The Hermes config.yaml schema has stabilized and users have migrated to the current provider fields, so the value of scanning for model.provider dangling references, custom_providers shape errors, v12 migration residue etc. no longer justifies the maintenance surface — and the scan produces false positives when users keep some providers under Hermes' v12+ providers: dict (Hermes' runtime merges both shapes, but CC Switch's scanner only looked at the list form). Removes the whole HermesHealthWarning type, scan_hermes_config_health command, HermesHealthBanner React component, useHermesHealth hook, warnings field on HermesWriteOutcome, and the three helper functions (yaml_as_non_empty_str, collect_mapping_string_keys, hermes_warning) that only served the scanner. Drops the matching i18n keys in zh/en/ja and the fixInWebUI button label that only the banner used. |
||
|
|
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> |
||
|
|
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> |
||
|
|
29e87487a1 |
fix(skills): prevent duplicate imports when import button is double-clicked (#2211)
Closes #2139 Two related defects let the installed-skills count balloon when users tap the import confirm button multiple times — either deliberately or because the button is still clickable while a slow import is in flight: - The confirm button only disabled itself while `selected.size === 0`, so it stayed clickable during a pending mutation. Each extra click triggered another `importFromApps` mutation. - `useImportSkillsFromApps` appended the server response to the installed cache without deduping, so re-firing the mutation stacked the same skills into the list again. Disable the confirm (and cancel) buttons while the mutation is pending — matching the `isRestoring` / `isDeleting` pattern already used by `RestoreSkillsDialog` — and merge success payloads by `InstalledSkill.id` so repeated results overwrite rather than accumulate. The merge is extracted as a pure `mergeImportedSkills` reducer to make the behaviour unit-testable and to short-circuit on an empty payload, returning the existing reference so React Query does not notify subscribers about a no-op cache update. |
||
|
|
7bfe2609db |
Style/session manager list UI (#2201)
* style(FailoverQueueManager): 显示供应商备注信息 * style(FailoverQueueItem): 添加供应商备注字段以支持备注信息显示 * style(FailoverQueueManager): 显示供应商备注信息 * style(FailoverQueueItem): 添加供应商备注字段以支持备注信息显示 * style(FailoverQueueManager): 更新供应商备注信息的显示样式 * style(FailoverQueueItem): 添加条件序列化以优化供应商备注字段 * style(App, SettingsPage, ScrollArea): 调整组件样式以改善布局和视觉效果 * style(App, SettingsPage, ScrollArea): 调整组件样式以改善布局和视觉效果 * style(SettingsPage, useSettings): 统一代码格式,调整样式和变量声明 * style(App): 调整底部内边距以改善布局 |
||
|
|
1b3c53d235 |
fix: always show name on hover (#2237)
Signed-off-by: tison <wander4096@gmail.com> |
||
|
|
4dd11ab427 |
fix(usage): support Hermes and OpenClaw in usage query modal
Extend getProviderCredentials to read flat settingsConfig fields for Hermes (snake_case base_url / api_key) and OpenClaw (camelCase baseUrl / apiKey), so the "official balance" template auto-selects for matching providers like SiliconFlow. Also refactor the BALANCE and TOKEN_PLAN test paths in handleTest to reuse the precomputed providerCredentials instead of re-reading env.ANTHROPIC_* directly, which previously caused empty key errors for non-Claude apps even when the key was configured. |
||
|
|
db92f2b3b0 |
style(providers): apply prettier to common-config hooks
Reformat three hooks brought in from upstream #2191 so that format:check passes in CI. |
||
|
|
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 |
||
|
|
111ddf8d73 |
style: apply prettier and rustfmt
No behavior changes. Brings three files back in line with the project formatters (CI runs `pnpm format:check` and `cargo fmt --check`). |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
3f4739365e |
feat(hermes): surface providers: dict entries read-only
Hermes v12+ migrated some provider entries from the `custom_providers:` list into a `providers:` dict (keyed by id). CC Switch previously ignored that source entirely, leaving users blind to providers they had configured via Hermes' own Web UI; the only feedback was a generic migration warning in the health banner. `get_providers()` now unions both sources, matching upstream `get_compatible_custom_providers` dedup order (list wins on name collision). Entries coming from the dict carry a `_cc_source = "providers_dict"` marker plus the original `provider_key`, which the UI layer will use to render them read-only. `set_provider` and `remove_provider` now refuse to touch dict-only entries, steering the user to Hermes Web UI. `sanitize_hermes_provider_keys` strips the UI markers on write so they never reach YAML. The `schema_migrated_v12` health warning copy reframes the situation: entries are shown read-only in CC Switch rather than invisible. |
||
|
|
e10a300c80 |
fix(hermes): prevent YAML pollution and drop of OAuth mcp auth
DeepLink Hermes import was emitting camelCase (baseUrl / apiKey / apiMode) that the Hermes runtime does not recognise, poisoning `custom_providers:` entries on activation. The MCP sync path was also stripping `auth: oauth` on round-trip, silently downgrading OAuth-type servers to unauthenticated calls. The Hermes deeplink branch now emits snake_case via a dedicated builder; `sanitize_hermes_provider_keys` runs on both `set_provider` and `get_providers` so legacy DB records heal on next access. `HERMES_EXTRA_FIELDS` preserves `auth`. The `api_mode` dropdown gains `codex_responses` (Copilot / OpenCode), and the schema-migrated warning copy no longer hard-codes "v12" (upstream `_config_version` is now 19). |
||
|
|
185ac2be9b |
feat(hermes): memory enable switch + clearer migration warning copy
Replaces the greyed-out "Memory is disabled" banner with a real Switch at the top of each memory tab. Users can now toggle Hermes' memory/user blobs without leaving CC Switch; the underlying write goes through the merge-aware `set_memory_enabled`, so budgets and external-provider settings survive toggle operations. The new `useToggleHermesMemoryEnabled` mutation invalidates the limits query so the Switch state and the amber disabled-hint update in lockstep. Reworks the `schema_migrated_v12` health banner copy to match the simplified "CC Switch only manages custom_providers" posture — it now tells users to reconcile migrated dict entries via Hermes Web UI, instead of the earlier (and now inaccurate) "CC Switch reads both". |
||
|
|
b8a3534cb5 |
feat(settings): add Hermes config dir override with data-driven dispatch
Adds a dedicated Hermes row to the directory-override settings so users can point CC Switch at alternate Hermes config locations (e.g. a second profile directory for work/personal split). `get_config_dir` on the Rust side already supports hermes; this just wires up the frontend row. Wiring it through `useDirectorySettings` revealed a scaling problem: every supported app required five parallel ternary chains across `computeDefaultConfigDir`, `updateDirectory`, `browseDirectory`, `resetDirectory`, and `updateDirectoryState`. Replaces those with two lookup tables (`APP_DIRECTORY_META`, `DIRECTORY_KEY_TO_SETTINGS_FIELD`) so adding the next app is two entries, not fifteen edit sites. Drive-by cleanup from the same touch: * `resetAllDirectories` takes a `ResolvedAppDirectoryOverrides` object instead of five positional optional strings. * `setResolvedDirs` returns the same reference when the sanitized value is unchanged, so no-op edits don't cascade renders. Also lands all i18n updates for this series (`hermesConfigDir` and placeholder, Memory section's enable/disable/toggleFailed copy, and the reworded `schemaMigratedV12` warning) in zh/en/ja together. |
||
|
|
acc6d795e4 |
feat(hermes): replace Prompts entry with Memory panel
Hermes has no slash-prompt concept (templates live as Skills), so the
Prompts tab for the Hermes app was always empty. Swap the toolbar Book
button for a Brain button that opens a new Memory panel editing
~/.hermes/memories/{MEMORY,USER}.md — Hermes' first-class memory store
which its Web UI exposes only as on/off toggles, never as an editor.
The panel shows each file in its own tab with a character-budget bar
read from config.yaml's nested memory.* section (memory_char_limit /
user_char_limit, default 2200 / 1375). Edits are written atomically;
Hermes picks them up on the next session start per MemoryStore.
Also extract useDarkMode to src/hooks/useDarkMode.ts — the codebase
already repeats the same MutationObserver pattern in 12+ places; this
PR introduces the shared hook and uses it once, leaving the migration
of the other copies to a follow-up.
|
||
|
|
088b47b08a |
refactor(hermes): delegate deep config to Hermes Web UI
Slim the Hermes surface in CC Switch to match its core positioning — cross-client provider switching and shared MCP/prompts/skills — and delegate deep configuration (model, agent, env, skills, cron, logs) to the Hermes Web UI at http://127.0.0.1:9119. - Drop AgentPanel/EnvPanel/ModelPanel and their mutation commands, hooks, types, and i18n keys across zh/en/ja. - Add open_hermes_web_ui Tauri command that probes /api/status and launches the URL in the system browser. Hermes injects its own session token into the returned HTML, so CC Switch doesn't need to touch auth. - Surface the launcher from the Hermes toolbar and the health banner via a shared useOpenHermesWebUI() hook; the offline error code is defined once per side and referenced across the contract. - Keep read-only access to model.provider so ProviderList can still highlight the active supplier; apply_switch_defaults continues to write the top-level model section when switching providers. Net diff: +152 / -1253. |
||
|
|
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. |
||
|
|
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). |
||
|
|
1684cb3233 |
feat(presets): migrate all aggregator and Bedrock presets to Claude Opus 4.7
- OpenClaw: replace opus-4-6 with opus-4-7 across 17 aggregator presets (id, name, primary, modelCatalog); AWS Bedrock entry rewritten to new SKU anthropic.claude-opus-4-7 (drops -v1 and dated suffix per official 4.7 model card) and pricing corrected to $5/$25/$0.50/$6.25 during the SKU swap, aligning with schema.rs source of truth - OpenCode: same replacement for 13 aggregators plus OPENCODE_PRESET_MODEL_VARIANTS entries for @ai-sdk/amazon-bedrock and @ai-sdk/anthropic, plus AWS Bedrock provider models map - OpenRouter / TheRouter / GitHub Copilot in claudeProviderPresets use dot-style id; update to anthropic/claude-opus-4.7 (missed by 509d2250) - omo: switch agent/category recommended to opus-4-7; replace key in OMO_BACKGROUND_TASK_PLACEHOLDER priority map - hermes_config.rs: update doc comments and test fixtures to opus-4-7; Hermes ModelPanel placeholder and i18n defaultHint examples follow - i18n unspecifiedHigh category description bumped to 'Claude Opus 4.7 max variant' to match omo recommended - Test fixtures updated: therouter preset assertion and opencode Bedrock variant lookup now check for opus-4-7 - Sonnet 4.6 / Haiku 4.5 untouched - no official 4.7 release for them |
||
|
|
50431b7ec9 |
feat(usage-script): add User-Agent header to New API template
Align the New API usage query template with the GENERAL template by including "User-Agent: cc-switch/1.0" in its request headers, so cc-switch requests are identifiable in provider server logs and less likely to be blocked by UA-based rate limiting on some New API deployments. |
||
|
|
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. |
||
|
|
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 |
||
|
|
e8953c286f |
feat: implement Hermes session manager with SQLite + JSONL support (Phase 6)
- Add hermes.rs session provider with dual-source scanning: SQLite (state.db) as primary, JSONL transcripts as fallback - Dynamic schema discovery via PRAGMA table_info for SQLite resilience - Use read_head_tail_lines for efficient JSONL metadata extraction (head 30 lines for metadata, tail 10 for last_active_at) - Support both flat and nested JSONL message formats - Add SQLite session loading and transactional deletion - Register hermes in parallel session scan (thread::scope) - Add "hermes" to frontend ProviderFilter type - 7 unit tests covering JSONL parsing, SQLite source parsing, deletion |
||
|
|
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) |
||
|
|
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) |
||
|
|
701e7d9581 |
fix: surface backend error details in proxy toast messages
The takeover.failed i18n template lacked the {{detail}} placeholder
and three useProxyStatus onError callbacks omitted the detail variable,
so proxy start/stop/takeover failures all displayed a generic message
regardless of the underlying cause.
|
||
|
|
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 |
||
|
|
eab0a69d2c | fix: unify weekly_limit tier label to match official 7-day naming | ||
|
|
4e790ac059 | fix: hide unknown subscription quota tiers from provider card UI | ||
|
|
cc77a040e2 |
fix: common config checkbox state not persisting for Codex/Claude/Gemini (#2191)
修复:Codex/Claude/Gemini 通用配置勾选状态无法正确保存的问题 Co-authored-by: 曾兴 <zengx@gantsoftware.com> |
||
|
|
34349a2743 |
Add OpenClaw config directory settings (#1518)
Co-authored-by: 张斌 <zhangbin25@xiaomi.com> |
||
|
|
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. |
||
|
|
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> |
||
|
|
1126c7459d |
Style/add provider.notes (#2138)
* style(FailoverQueueManager): 显示供应商备注信息 * style(FailoverQueueItem): 添加供应商备注字段以支持备注信息显示 * style(FailoverQueueManager): 显示供应商备注信息 * style(FailoverQueueItem): 添加供应商备注字段以支持备注信息显示 * style(FailoverQueueManager): 更新供应商备注信息的显示样式 * style(FailoverQueueItem): 添加条件序列化以优化供应商备注字段 |
||
|
|
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
|
||
|
|
de23216e49 |
feat(usage): refine usage dashboard UI and date range picker (#2002)
* feat(usage): enhance usage stats backend and query hooks * feat(usage): redesign calendar date range picker with auto-switch and simplified layout * refactor(usage): streamline dashboard layout and stats components * refactor(usage): compact request log table with merged cache/multiplier columns and centered layout * feat(i18n): add cache short labels and usage stats translations for zh/en/ja * Align usage dashboard stats with range boundaries The usage dashboard mixed second-precision detail rows with day-level rollups, which caused custom half-day ranges to overcount historical rollup data and left the request log paginator on stale pages after top-level filter changes. This change limits rollups to fully covered local days, aligns multi-day trend buckets with natural local days, and resets request log pagination when the dashboard range or app filter changes. Constraint: usage_daily_rollups stores only daily aggregates after pruning old detail rows Rejected: Include partial boundary rollups proportionally | historical intra-day detail is unavailable after pruning Rejected: Force RequestLogTable remount on range change | would discard local draft filters unnecessarily Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep summary, trends, provider stats, and model stats on the same rollup-boundary rules Tested: cargo test --manifest-path src-tauri/Cargo.toml usage_stats Tested: pnpm exec vitest run tests/components/RequestLogTable.test.tsx Tested: pnpm typecheck Not-tested: Manual UI validation in the Tauri app * Preserve full-day usage filters at minute precision The latest review surfaced two interaction bugs in the usage dashboard: rollup-backed stats undercounted end days selected via the minute-precision picker, and immediate select changes accidentally applied unsubmitted text drafts from the request log filters. This change treats 23:59 as a fully selected local end day for rollup inclusion and narrows select-side state syncing so app/status updates do not commit provider/model drafts. Constraint: The custom range picker emits minute-precision timestamps, while rollups are stored at day granularity Rejected: Require exact 23:59:59 end timestamps | unreachable from the current picker UI Rejected: Rebuild applied filters from the full draft state on select changes | silently commits unsaved text input Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep request-log text fields on explicit apply semantics even when select filters remain immediate Tested: cargo test --manifest-path src-tauri/Cargo.toml usage_stats Tested: pnpm exec vitest run tests/components/RequestLogTable.test.tsx Tested: pnpm typecheck Not-tested: Manual Tauri dashboard interaction * refactor(usage): move range presets into date picker, single-row layout - UsageDateRangePicker: add preset shortcuts (今天/1d/7d/14d/30d) inside popover top; clicking a preset applies immediately and closes popover - UsageDashboard: collapse to single row (app filters + refresh + picker); remove standalone preset buttons and summary stats bar - RequestLogTable: replace static Calendar badge with interactive UsageDateRangePicker via onRangeChange prop; single filter row * Keep usage pagination regression coverage aligned with the rendered UI The new regression test was asserting a non-existent pagination label and page summary text, so it failed before it could verify the real page-reset behavior. This commit switches the assertions to the numbered pagination buttons that the component actually renders and validates the reset through the query hook arguments. Constraint: RequestLogTable exposes numbered pagination buttons, not a "Next page" label or "2 / 6" summary text Rejected: Add synthetic pagination labels solely for the test | would couple production markup to a test-only assumption Confidence: high Scope-risk: narrow Reversibility: clean Directive: Prefer pagination assertions that follow the rendered controls or hook inputs instead of invented summary text Tested: pnpm vitest run tests/components/RequestLogTable.test.tsx; pnpm typecheck; pnpm test:unit * refactor(usage): clean up dead code and polish date range picker - Remove unused exports MAX_CUSTOM_USAGE_RANGE_SECONDS, timestampToLocalDatetime, and localDatetimeToTimestamp from usageRange.ts (replaced by the calendar picker) - Deduplicate getPresetLabel from UsageDashboard and UsageDateRangePicker into shared getUsageRangePresetLabel helper - Add aria-label, aria-current and aria-pressed to calendar day buttons so screen readers can disambiguate same-numbered days across adjacent months - Drop unused cacheReadShort and cacheWriteShort i18n keys (zh/en/ja); the request log table renders R/W prefixes inline - Align customRangeHint copy with the removed 30-day limit by dropping "up to 30 days" wording (zh/en/ja) * fix(usage): align rollup cutoff to local midnight to keep days complete `rollup_and_prune` previously used `Utc::now() - retain_days * 86400` as the cutoff. Because rollups are bucketed by *local* date and detail rows below the cutoff are pruned, an unaligned cutoff left the youngest rolled-up day half-rolled-up and half-pruned. Combined with the new `compute_rollup_date_bounds` boundary trimming (which excludes any rollup day not fully covered by the requested range), custom range queries that touch that day silently under-count summary, trend, provider, and model stats. Fix the invariant at the source: snap the cutoff to the next local midnight after `(now - retain_days)`. Every rollup row now reflects a complete local day, so the boundary trimmer's all-or-nothing assumption holds. Includes unit tests for the cutoff math (typical case + already-on- midnight case). DST gap is handled defensively by bumping forward by an hour. Addresses Codex P2 review finding on PR #2002. --------- Co-authored-by: Jason <farion1231@gmail.com> |
||
|
|
507bf038a9 |
feat(stream-check): refresh default models and detect model-not-found errors (#2099)
* chore(stream-check): update default health check models to latest Replaces deprecated gpt-5.1-codex@low with gpt-5.4@low and switches the Gemini default from gemini-3-pro-preview to gemini-3-flash-preview to pick the lightest variant of the latest series for fast, low-cost health checks. https://claude.ai/code/session_01NGWLchcTP76rJHjiP5Ehte * feat(stream-check): detect model-not-found errors with dedicated toast Health check previously classified failures purely by HTTP status code, which meant deprecated/invalid models showed up as a generic "Not found (404)" error pointing users to check the Base URL — misleading when the URL is fine and only the test model is wrong (e.g. gpt-5.1-codex after it was retired). Backend: add detect_error_category() that inspects 4xx response bodies for model-not-found indicators (model_not_found, does not exist, invalid model, not_found_error, etc.) and returns a "modelNotFound" category. Thread the resolved test model through build_stream_check_result so the failed result carries it in model_used. Add StreamCheckResult .error_category field (serde-skipped when None). Frontend: useStreamCheck branches on errorCategory === "modelNotFound" before the HTTP-status fallback and renders a toast.error with the model name and a description pointing to Model Test Config. Add i18n keys (modelNotFound / modelNotFoundHint) for zh/en/ja. Tests: unit-test detect_error_category against real OpenAI/Anthropic error shapes, 5xx false-positive avoidance, and plain 401 auth errors. https://claude.ai/code/session_01NGWLchcTP76rJHjiP5Ehte * fix(stream-check): add missing error_category field in fallback The error_category field was added to StreamCheckResult in this branch but the fallback constructor in stream_check_all_providers was not updated, which broke cargo build. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
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. |