Commit Graph

1733 Commits

Author SHA1 Message Date
Roger Deng 6e8ee094f1 fix(usage): reduce price input step to 0.0001 for sub-cent precision (#2793)
The step was 0.01, preventing input of prices like DeepSeek's cache read
cost ($0.0028/million tokens). Extract step value to a constant and apply
to all four price fields.

Closes #2503
2026-05-18 11:38:32 +08:00
luw2007 5ae9c2605d fix(terminal): Ghostty opens clean window instead of cloning existing tabs (#2801)
When Ghostty is already running, `open -a` silently ignores `--args`,
and `open -na` clones all existing tabs into the new instance.

Add a dedicated `launch_macos_ghostty` that uses
`--quit-after-last-window-closed=true` and `-e bash <script>` to spawn
a single clean window running claude.

Also change `launch_macos_open_app` from `open -a` to `open -na` so
other terminals (Alacritty/Kitty/WezTerm/Kaku) correctly open a new
window when already running.

Closes #2798
2026-05-18 11:37:27 +08:00
BlueOcean 0fb7fd12e5 feat: add Xiaomi MiMo Token Plan presets (#2803)
* feat: add Xiaomi MiMo token plan presets

* fix: update Xiaomi MiMo provider presets

* fix: align MiMo V2.5 model specs with official documentation

- Update maxTokens from 32000 to 131072 (128K) for mimo-v2.5-pro and mimo-v2.5
- Update contextWindow from 262144 to 1048576 (1M) for mimo-v2.5
- Aligns with official specs from Xiaomi MiMo documentation
- Ensures consistency between OpenClaw and OpenCode presets

---------

Co-authored-by: Jason <farion1231@gmail.com>
2026-05-18 10:38:03 +08:00
Tiancrimson 5c79cf64a4 fix(gemini-native): resolve functionResponse.name and replay thought_signature for synthesized tool call IDs (#2814)
* fix(gemini-native): resolve functionResponse.name and thought_signature replay for synthesized tool call IDs

Two related bugs in the Gemini Native format conversion layer:

1. **functionResponse.name resolution** (422 error): When Gemini's parallel
   function calls omit the id field, cc-switch synthesizes gemini_synth_*
   IDs. These are stored in the shadow store but can be lost in long sessions,
   causing subsequent tool_result blocks to fail. Fix: pre-scan all assistant
   messages in the request body to seed the tool_name_by_id map, and add a
   last-resort fallback that scans the current content array for matching
   tool_use blocks.

2. **thought_signature replay** (400 error): The Anthropic Messages format
   strips thoughtSignature from tool_use blocks, but Gemini requires it on
   every functionCall in multi-turn tool-use exchanges. Fix: build a
   thought_signature_by_id map from shadow turns and attach thoughtSignature
   when converting tool_use back to functionCall.

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

* style: run cargo fmt on transform_gemini.rs

---------

Co-authored-by: Tiancrimson <tiancrimson@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Jason <farion1231@gmail.com>
2026-05-18 10:25:35 +08:00
LaoYueHanNi bbce75fcda fix(session): 修复session log模式下子Agent token统计遗漏 (#2821)
* fix(session): 修复session log模式下子Agent token统计遗漏

collect_jsonl_files() 只扫描了两层目录,遗漏了子Agent的JSONL日志文件,
导致子Agent的独立token使用数据完全未统计到session费用中。
(仅影响session log模式,proxy代理模式不受影响)

* refactor(session): optimize collect_jsonl_files logic

- Replace two independent if statements with if-else for mutually exclusive conditions
- Remove unnecessary clone() when pushing file paths
- Add clarifying comments for main session vs subagent files
- Apply cargo fmt for consistent formatting

Performance improvement: Eliminates redundant clone() operations when
processing .jsonl files, as a path cannot be both a file and a directory.

---------

Co-authored-by: Jason <farion1231@gmail.com>
2026-05-18 10:00:57 +08:00
iamryan ed33990b9c fix-codex-mise-detection (#2822) 2026-05-18 09:38:08 +08:00
Zylo 8786f44c5a Fix race condition in useEffect hooks and type assertion bug (#2827)
- Add active flag pattern to 3 useEffect hooks in App.tsx to prevent
  event listener leaks when component unmounts before async setup completes
- Add guard check in useSettings.ts to prevent undefined from being
  stored in localStorage when payload.language is missing

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 09:36:44 +08:00
Jason 4f0f103a8a Add Claude Desktop user guide docs
- Add the Claude Desktop provider guide in English, Chinese, and Japanese.
- Add localized screenshots for import, provider setup, model mapping, and local routing.
- Link the guide from the v3.15.0 release notes and user manual indexes.
2026-05-16 20:18:49 +08:00
Jason c460a404dc Add Claude Desktop official preset
- Add Claude Desktop Official to the Claude Desktop preset list.
- Treat selected official presets as official mode in the form.
- Cover the official preset with a preset-order regression test.
2026-05-16 19:49:59 +08:00
Jason 6172bfd549 docs(release-notes): soften imposter warning wording in en/ja notes 2026-05-16 13:26:53 +08:00
Jason 1232b49b74 docs(release-notes): soften imposter warning wording in zh notes 2026-05-16 13:25:09 +08:00
Jason 3640a4e27d docs(release-notes): remove emoji from imposter site warning 2026-05-16 13:21:44 +08:00
Jason fed892d307 docs(release-notes): add imposter site warning to v3.15.0 notes 2026-05-16 13:19:33 +08:00
Jason 9e3f168903 chore(release): bump version to 3.15.0 v3.15.0 2026-05-16 11:21:18 +08:00
Jason 9e6a7a0d81 - Fix proxy test helper clippy warning
- Mark `should_force_identity_encoding` as test-only.

- Keep runtime forwarding behavior unchanged.

- Verified with local CI checks and no-bundle Tauri build.
2026-05-15 23:45:26 +08:00
Jason 4a631b28c7 chore(i18n): simplify modelMappingOffHint to action-oriented copy 2026-05-15 23:33:48 +08:00
Jason ec8afd63ea refactor(presets): render presets in array order and prioritize partners
Remove the category-based grouping logic from ProviderPresetSelector,
letting the array position in each preset config file be the single
source of truth for display order. Move partner presets (PatewayAI,
火山Agentplan, BytePlus, DouBaoSeed) right after Shengsuanyun across
all 6 config files so they appear earlier in the UI.
2026-05-15 23:28:59 +08:00
Jason 9050442b65 feat(presets): add BytePlus provider preset as partner
Add BytePlus (international Volcengine) to Claude, Claude Desktop,
Hermes, OpenCode, and OpenClaw with byteplus icon, 256K context window,
and trilingual promotion text.
2026-05-15 22:45:04 +08:00
Jason d94eb6729b feat(presets): add 火山Agentplan provider preset as partner
Add Volcengine Ark Agentplan to Claude, Claude Desktop, Hermes,
OpenCode, and OpenClaw with huoshan icon and trilingual promotion.
2026-05-15 22:28:26 +08:00
Jason a0131c9a2b chore(presets): update DouBaoSeed endpoint, links, and add as partner
Switch Anthropic-format base URL from /api/coding to /api/compatible,
update website/apiKey URLs to Volcengine console with tracking params,
and promote DouBaoSeed to partner with trilingual promotion text.
2026-05-15 22:00:55 +08:00
Jason 58cd5302ae chore(presets): switch RelaxyCode icon to custom relaxcode.png
Replace the generic claude icon with a dedicated relaxcode.png icon
for the RelaxyCode provider presets.
2026-05-15 17:55:47 +08:00
Jason 3fd38b0a3d feat(presets): add RelaxyCode provider presets
Add RelaxyCode as a new third-party provider with support for:
- Claude Code preset (Anthropic native protocol)
- Codex preset (gpt-5.5 model)
- Claude Desktop preset (direct mode with passthrough routes)

RelaxyCode is an enterprise-grade AI programming platform providing
unified access to Claude Code, Codex, and Gemini CLI models.
2026-05-15 17:38:26 +08:00
Jason 18ffddbfa5 feat(presets): add RunAPI provider presets
Add RunAPI as a new partner provider with support for:
- Claude Code preset (Anthropic native protocol)
- Codex preset (gpt-5.5 model)
- Claude Desktop preset (direct mode with passthrough routes)
- OpenCode preset (@ai-sdk/anthropic)
- OpenClaw preset (anthropic-messages protocol)
- Hermes preset (anthropic_messages mode)
- Icon configuration (runapi.jpg)
- i18n support (zh/en/ja) with ¥14 free credit promotion

RunAPI is a high-performance AI model API gateway supporting 150+
mainstream models (OpenAI, Claude, Gemini, DeepSeek, Grok) with
prices as low as 10% of official rates.
2026-05-15 17:20:46 +08:00
Jason d6bbbf72f0 feat(presets): add ClaudeCN provider presets
Add ClaudeCN as a new partner provider with support for:
- Claude Code preset (Anthropic native protocol)
- Codex preset (gpt-5.5 model)
- Claude Desktop preset (direct mode with passthrough routes)
- OpenCode preset (@ai-sdk/anthropic)
- OpenClaw preset (anthropic-messages protocol)
- Hermes preset (anthropic_messages mode)
- Icon configuration (claudecn.png)
- i18n support (zh/en/ja) with enterprise service promotion

ClaudeCN is an enterprise-grade AI gateway operated by a registered
company, supporting enterprise procurement processes with corporate
payments, contracts, and compliance guarantees.
2026-05-14 23:28:07 +08:00
Jason df11df4d9c feat(presets): add ClaudeAPI provider presets
Add ClaudeAPI as a new partner provider with support for:
- Claude Code preset (using ANTHROPIC_AUTH_TOKEN field)
- Claude Desktop preset (direct mode with passthrough routes)
- Icon configuration (ClaudeApi.png)
- i18n support (zh/en/ja) with test credit promotion

ClaudeAPI provides official Anthropic API keys and AWS Bedrock
routing with support for Tool Use and 1M context.
2026-05-14 23:07:49 +08:00
Jason 4eb5543d9f refactor(presets): switch 20 Claude Desktop providers from proxy to direct mode
- Change mode from "proxy" to "direct" for 20 third-party/aggregator providers
- Simplify PipeLLM from mappedRoutes to passthroughRoutes for consistency
- Reduces unnecessary proxy layer overhead for providers that support direct API calls

Affected providers: ShengSuanYun, AIHubMix, DMXAPI, PackyAPI, PatewayAI,
Cubence, AIGoCode, RightCodes, AICodeMirror, AICoding, CrazyRouter,
SSSAICode, ModelVerse, CompShare, MicuAPI, CTOK, E-FlowCode,
VibeCodingAPI, LemonData, PipeLLM
2026-05-14 22:52:54 +08:00
Jason 08cd5ab56e feat(presets): add PatewayAI provider presets
Add PatewayAI as a new partner provider with support for:
- Claude Code preset (using ANTHROPIC_API_KEY field)
- Codex preset (gpt-5.5 model)
- Claude Desktop preset (proxy mode with passthrough routes)
- Icon configuration (pateway.jpg)
- i18n support (zh/en/ja) with $3 registration bonus promotion

PatewayAI provides reliable API routing services for Claude Code,
Codex, and Gemini models.
2026-05-14 22:36:45 +08:00
Jason b642ef0633 fix(failover): patch P1-P3 reliability gaps surfaced by team review
- Forwarder buffers non-streaming bodies and primes streaming first
  chunk before signaling success, so body timeouts and SSE first-chunk
  failures route through the circuit breaker instead of being recorded
  as success on response-header arrival
- Atomic enable-failover: switch to P1 before persisting the flag, and
  roll back auto-added queue entries when the switch is rejected
  (e.g. official providers)
- Hot-reload circuit breaker config on per-app proxy config change
  instead of waiting for a proxy restart
- FailoverToggle / FailoverQueueManager / AutoFailoverConfigPanel
  require proxy takeover for the active app; the backend command also
  rejects enabling when takeover is off
- ProviderHealthBadge consumes the backend is_healthy flag instead of
  hardcoding the 5-failure threshold

Cleanup:
- impl From<&AppProxyConfig> for CircuitBreakerConfig and use it from
  the command layer
- Collapse three identical TabsContent blocks into a single map
2026-05-14 21:35:02 +08:00
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 0d09555503 chore(partners): cross-link BytePlus/Volcengine entries in READMEs 2026-05-14 16:13:35 +08:00
Jason cb45c22b63 chore(presets): migrate OpenClaudeCode to MicuAPI domain
Replace all openclaudecode.cn URLs with micuapi.ai across all
provider presets (Claude, Codex, Hermes, OpenClaw, OpenCode,
Claude Desktop). This includes website URLs, API key URLs, and
base URLs.
2026-05-14 15:41:52 +08:00
Jason 8dabb9fab9 chore(presets): update CrazyRouter API endpoints to cn subdomain
Update all CrazyRouter baseURL configurations from crazyrouter.com
to cn.crazyrouter.com across all supported applications (Claude,
Codex, Gemini, Hermes, OpenClaw, OpenCode, Claude Desktop).

Website and registration URLs remain unchanged.
2026-05-14 15:40:41 +08:00
Jason 99304ffcfd chore(partners): remove DDSHub partner integration
Remove DDSHub from all provider presets (Claude, Claude Desktop, Codex, Hermes),
i18n files (zh/en/ja), README docs, and icon system. Physical assets retained for
potential future restoration.
2026-05-14 15:35:35 +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 f93b935d5f fix(proxy): expose real provider model names in Claude Code menu under takeover
When proxy takeover is active, write per-role *_MODEL aliases for routing
and *_MODEL_NAME with the upstream provider's real model name so the
Claude Code model menu reflects the active provider instead of stale
display names from a previous switch. Preserves the [1M] capability marker
for Sonnet/Opus, and strips it from implicit display names.
2026-05-14 12:16:00 +08:00
Jason 402570ce31 fix(usage): pricing routing, SSE lifecycle, and validation hardening
* model pricing routing: extend prefix-match families (gpt-/o1-o5/
  gemini-/deepseek-/qwen-/glm-/kimi-/minimax-) with per-family dash
  thresholds so short base IDs like gpt-5 no longer mis-match
  gpt-5-mini; strip ISO and 8-digit date suffixes via UTF-8-safe
  byte matching so claude-haiku-4-5-20251001 falls back to
  claude-haiku-4-5 pricing
* SSE collector: SseUsageFinishGuard (RAII) guarantees finish() on
  early return or panic; AtomicBool fast path lets push() skip the
  Mutex once first-event time is recorded
* validation: shared validate_cost_multiplier / validate_pricing_source
  helpers across DAO and service layers; PRICING_SOURCE_RESPONSE /
  PRICING_SOURCE_REQUEST constants replace string literals; price
  fields in update_model_pricing now reject empty / non-decimal /
  negative input before INSERT
* backfill: add backfill_missing_usage_costs_for_model so a single
  price edit only scans matching rows instead of the full log table;
  startup backfill remains full-scan
* session_usage{,_codex,_gemini}: share find_model_pricing helper from
  usage_stats; metadata_modified_nanos centralizes mtime precision
* frontend: NON_NEGATIVE_DECIMAL_REGEX + isNonNegativeDecimalString
  replace three copies of the same multiplier regex; isUnpricedUsage
  surfaces zero-cost rows that have usage tokens (cached per row to
  avoid double evaluation); invalidate usageKeys.all on pricing mutate
  so backfilled rows refresh
2026-05-14 11:53:51 +08:00
Jason 206125b4e3 fix(proxy): patch P0-P3 routing/lifecycle issues across forwarder paths
* stream_check: thread Result from get_auth_headers via map_err so
  the workspace builds again
* forwarder: scope rectifier / budget-rectifier flags per-provider so
  failover can still apply rectification on the next attempt
* forwarder: categorize before record_result; route NonRetryable and
  ClientAbort through release_permit_neutral so client-side failures
  don't pollute circuit breaker or DB health
* handler_context: parse Gemini model from uri.path() and strip both
  ?query and :action verb defensively in extract_gemini_model_from_path
* forwarder + response_processor + handlers: introduce
  ActiveConnectionGuard (RAII) so active_connections decrement covers
  the full streaming body lifetime, not just response headers
* claude_desktop_config: use sort_by_key to clear the clippy gate
2026-05-14 09:23:21 +08:00
Jason 85131d37d8 refactor(proxy): extract handle_rectifier_retry_failure helper
The signature (RECT-003) and budget (RECT-012) rectifier branches each
carried ~50 lines of identical "provider error -> record + continue /
client error -> release permit + return" handling. The only piece that
varied between them was a log label ("整流" vs "budget 整流").

Move the shared logic into RequestForwarder::handle_rectifier_retry_failure
that returns Option<ForwardError> — None means "continue to the next
provider", Some(err) means "terminal failure, return to the client".
Each call site shrinks from ~50 lines to ~17, drops one level of
indentation, and the two branches now provably cannot drift apart.

forwarder.rs nets ~40 lines smaller.
2026-05-14 08:22:07 +08:00
Jason 039784af73 refactor(proxy): share auth_header_value helper across provider adapters
claude.rs and gemini.rs each defined an identical `hv` closure that wrapped
`HeaderValue::from_str` into a ProxyError::AuthError result, and codex.rs
spelled the same conversion out inline. /simplify reviewers flagged this
as drift-prone copy-paste.

Move the conversion into a single `pub fn auth_header_value` in
providers/adapter.rs and have the three adapters import it locally. Same
error wording everywhere, one place to update if HeaderValue semantics
ever change.
2026-05-14 08:21:53 +08:00
Jason 3c35972548 fix(proxy-ui): accept IPv6 listen addresses in ProxyPanel validation
The backend already understands `::` -> `::1` and wraps IPv6 literals
in brackets (services/proxy.rs), but the panel's save-time validator
only accepted localhost, 0.0.0.0, and IPv4 dotted-quads. Users who
wanted to listen on an IPv6 loopback had to bypass the UI and edit
config directly.

Add an isValidIpv6 helper that requires at least one ':' and round-trips
through `new URL('http://[<addr>]/')` so the platform's built-in IPv6
parser does the heavy lifting (covers compressed `::`, full 8-group
form, zone IDs). Update the invalidAddress copy in zh / en / ja so the
error message reflects the new accepted set.
2026-05-13 23:36:44 +08:00
Jason 5d3d9067af feat(proxy): forward client HTTP method instead of hard-coding POST
The forwarder used to call client.post(&url) / http::Method::POST in
both the reqwest and hyper paths, and the Gemini route table only
registered POST /v1beta/*. As a result anything the Gemini SDK / CLI
sent as GET (models list, models/<id> info) hit a 404 at the router
and bypassed the local proxy's stats, rectifiers, and failover.

Thread the request method end-to-end:

- ProviderAdapter forwarder API now takes the http::Method by reference
  per attempt and dispatches client.request(method, &url) for reqwest
  and method.clone() for the hyper raw path.
- All five callers in handlers.rs (handle_messages_for_app for Claude /
  Claude Desktop, handle_chat_completions, handle_responses,
  handle_responses_compact, handle_gemini) pull the method out of the
  incoming axum::extract::Request and pass it on.
- handle_gemini tolerates an empty body (GET endpoints have none) and
  the forwarder skips serializing / sending a body for GET / HEAD —
  attaching JSON to a GET makes Gemini reject the request.
- server.rs swaps the Gemini routes to any(handle_gemini) so the same
  handler handles GET / POST / PUT / DELETE, and adds /gemini/v1/*
  for the GA path version.
2026-05-13 23:36:36 +08:00
Jason f2ae9823cb fix(proxy): move client-request counters out of per-attempt loop
Three statistics-shape issues fixed together so the dashboard reflects
client requests, not provider attempts:

1. active_connections never moved off zero — the field had no caller in
   the entire crate. Wrap forward_with_retry into a thin entry point
   that saturating_add(1) on enter and saturating_sub(1) on exit; every
   inner return path is covered automatically.

2. total_requests counted attempts, not requests. A single client call
   that failed over P1 -> P2 -> success was recorded as
   total=2 / success=1 -> 50% success rate. Move the increment and the
   last_request_at refresh into the wrapper so they fire once per
   client request regardless of how many providers were tried.

3. current_provider / current_provider_id stay inside the inner loop
   because they are intentionally per-attempt ("what am I trying right
   now?") — moving them would break the live-failover indicator.

Refactor: split forward_with_retry into a public wrapper + private
forward_with_retry_inner. Every existing `return Err(...)` inside inner
remains correct because the wrapper always runs the decrement on its
return.
2026-05-13 23:28:16 +08:00
Jason b06e0fa538 fix(proxy): wire AppProxyConfig.max_retries into request forwarder
The UI has exposed "请求失败时的重试次数 (0-10, default 3)" since the
auto-failover panel was added, but the value was silently dropped —
RequestForwarder never received it and the per-provider loop walked the
whole list regardless. From the user's perspective the setting was
inert.

Thread AppProxyConfig.max_retries through create_forwarder into
RequestForwarder, derive max_attempts = max_retries + 1 (so max_retries=0
matches the UI copy "0 retries" = single attempt), and break the loop
once attempts hit the cap. The check is placed before the circuit
breaker allow-permit so an over-cap iteration does not waste a HalfOpen
probe slot.

When auto-failover is disabled we also force max_retries to 0, mirroring
how timeouts already bypass in that mode — "no failover" should mean
"one provider, one try", not "limited retries against the same list".
2026-05-13 23:25:15 +08:00
Jason 84aa87c3dd fix(proxy): map Anthropic tool_choice to OpenAI Chat nested form
The Chat-Completions transformer used to forward tool_choice verbatim,
but the two APIs disagree on shape:

  Anthropic   "any" | {"type":"tool","name":"X"}
  OpenAI Chat "required" | {"type":"function","function":{"name":"X"}}

Pass-through made the upstream return 400 for any tool-forcing client
(Claude Code, Copilot, etc.). The Responses-API transformer already had
the equivalent map_tool_choice_to_responses helper; this commit adds a
sibling map_tool_choice_to_chat with the chat-specific *nested* function
selector and five regression tests covering string / object × any /
auto / none / tool.

The two helpers are intentionally not merged: the difference between
flat and nested function selectors is exactly what the original bug
was, so keeping them as separate self-documenting functions reduces the
chance of the same regression returning.
2026-05-13 23:20:55 +08:00
Jason cb4ecd3951 fix(proxy): refine failover decisions in forwarder
Two related changes to make per-provider failover behave correctly.

1. Bucket UpstreamError by status code in categorize_proxy_error.

   The old "every UpstreamError is Retryable" rule meant a malformed
   client request (400 / 422) would be replayed against every provider
   in the queue: errors amplified N-fold, the circuit breaker accrued
   unwarranted failure counts, and quota was burned. Now
   400 / 405 / 406 / 413 / 414 / 415 / 422 / 501 are NonRetryable since
   the request itself is wrong and no provider will accept it.
   401 / 403 / 404 / 408 / 409 / 429 / 451 and all 5xx remain Retryable
   because the next provider may carry a different key, quota, region,
   or model mapping.

2. Make the rectifier-retry path participate in failover.

   Both the signature (RECT-003) and budget (RECT-012) rectifier branches
   used to "return Err(...)" after the retry failed, short-circuiting the
   per-provider loop. A provider-side failure (5xx / Timeout /
   ForwardFailed) now records the circuit breaker, accumulates into
   last_error / last_provider, and "continue"s to the next provider —
   matching the normal Retryable arm. Client-side failures still return
   immediately since a different provider cannot fix a malformed payload.
2026-05-13 23:20:45 +08:00
Jason c3d810a22b fix(proxy): tighten takeover detection and use fallback restore on disable
Two related drift bugs in the takeover state machine:

1. The "already taken over?" guard used has_backup OR live_taken_over, so
   either condition alone would short-circuit. After a user or anomalous
   flow restores Live manually the backup row still made set_takeover
   return success, leaving the UI claiming takeover while requests bypass
   the local proxy. Tighten to AND so the rebuild branch repairs the two
   "split brain" states (backup-only and placeholder-only).

2. Disabling takeover called the bare restore_live_config_for_app, which
   silently Ok()s when the backup is missing. If the backup was lost while
   Live still held proxy placeholders (PROXY_MANAGED token / local proxy
   URL), the client config was left broken with no error surfaced. Route
   the disable path through the already-existing
   restore_live_config_for_app_with_fallback (backup → SSOT → cleanup).
   The line 354 takeover-failure rollback intentionally keeps the bare
   variant since that path must preserve the backup for retry.
2026-05-13 23:12:15 +08:00
Jason 9a8f52021d fix(proxy): extract Gemini request model from URI path correctly
split('/') strips the slashes, so find(|s| s.starts_with("models/")) never
matched any segment and request_model fell through to "unknown" for every
Gemini call, poisoning usage records, per-request billing, and logs.

Match the literal "models" segment and take the next one, stripping any
:action suffix and query string. The extraction is now a pub(crate) free
function so it can be unit-tested directly; seven regression tests cover
action suffixes, dotted versions, the /gemini/ proxy prefix, query
strings, the bare list endpoint, and missing-segment paths.
2026-05-13 23:12:06 +08:00
Jason c9a6afc0b7 fix(proxy): return Result from get_auth_headers to avoid panic on bad credentials
User-pasted API keys can contain control chars or CR/LF that make
HeaderValue::from_str return Err; the previous unwrap inside every
adapter turned such input into a process-wide panic instead of a request
error. The trait now returns Result<_, ProxyError>; Claude/Codex/Gemini
impls propagate ProxyError::AuthError so the client sees a 401 with the
underlying parse error instead of a crash. Adds a regression test that
pastes a CRLF-containing key and asserts AuthError.
2026-05-13 23:12:00 +08:00
Jason 58648a9c53 chore: drop trailing blank line in sql_helpers tests
Rustfmt cleanup, no behavioral change.
2026-05-13 23:11:52 +08:00