feat(proxy): add thinking signature rectifier for Claude API (#595)

* feat(proxy): add thinking signature rectifier for Claude API

Add automatic request rectification when Anthropic API returns signature
validation errors. This improves compatibility when switching between
different Claude providers or when historical messages contain incompatible
thinking block signatures.

- Add thinking_rectifier.rs module with trigger detection and rectification
- Integrate rectifier into forwarder error handling flow
- Remove thinking/redacted_thinking blocks and signature fields on retry
- Delete top-level thinking field when assistant message lacks thinking prefix

* fix(proxy): complete rectifier retry path with failover switch and chain continuation

- Add failover switch trigger on rectifier retry success when provider differs from start
- Replace direct error return with error categorization on rectifier retry failure
- Continue failover chain for retryable errors instead of terminating early

* feat(proxy): add rectifier config with master switch

- Add RectifierConfig struct with enabled and requestThinkingSignature fields
- Update should_rectify_thinking_signature to check master switch first
- Add tests for master switch functionality

* feat(db): add rectifier config storage in settings table

Store rectifier config as JSON in single key for extensibility

* feat(commands): add get/set rectifier config commands

* feat(ui): add rectifier config panel in advanced settings

- Add RectifierConfigPanel component with master switch and thinking signature toggle
- Add API wrapper for rectifier config
- Add i18n translations for zh/en/ja

* feat(proxy): integrate rectifier config into request forwarding

- Load rectifier config from database in RequestContext
- Pass config to RequestForwarder for runtime checking
- Use should_rectify_thinking_signature with config parameter

* test(proxy): add nested JSON error detection test for thinking rectifier

* fix(proxy): resolve HalfOpen permit leak and RectifierConfig default values

- Fix RectifierConfig::default() to return enabled=true (was false due to derive)
- Add release_permit_neutral() for releasing permits without affecting health stats
- Fix 3 permit leak points in rectifier retry branches
- Add unit tests for default values and permit release

* style(ui): format ProviderCard style attribute

* fix(rectifier): add detection for signature field required error

Add support for detecting "signature: Field required" error pattern
in the thinking signature rectifier. This enables automatic request
rectification when upstream API returns this specific validation error.
This commit is contained in:
Dex Miller
2026-01-14 00:12:13 +08:00
committed by GitHub
parent 53f40b2d7a
commit f3343992f2
18 changed files with 984 additions and 9 deletions
+69
View File
@@ -151,6 +151,24 @@ impl ProviderRouter {
self.reset_circuit_breaker(&circuit_key).await;
}
/// 仅释放 HalfOpen permit,不影响健康统计(neutral 接口)
///
/// 用于整流器等场景:请求结果不应计入 Provider 健康度,
/// 但仍需释放占用的探测名额,避免 HalfOpen 状态卡死
pub async fn release_permit_neutral(
&self,
provider_id: &str,
app_type: &str,
used_half_open_permit: bool,
) {
if !used_half_open_permit {
return;
}
let circuit_key = format!("{app_type}:{provider_id}");
let breaker = self.get_or_create_circuit_breaker(&circuit_key).await;
breaker.release_half_open_permit();
}
/// 更新所有熔断器的配置(热更新)
pub async fn update_all_configs(&self, config: CircuitBreakerConfig) {
let breakers = self.circuit_breakers.read().await;
@@ -325,4 +343,55 @@ mod tests {
assert!(router.allow_provider_request("b", "claude").await.allowed);
}
#[tokio::test]
async fn test_release_permit_neutral_frees_half_open_slot() {
let db = Arc::new(Database::memory().unwrap());
// 配置熔断器:1 次失败即熔断,0 秒超时立即进入 HalfOpen
db.update_circuit_breaker_config(&CircuitBreakerConfig {
failure_threshold: 1,
timeout_seconds: 0,
..Default::default()
})
.await
.unwrap();
let provider_a =
Provider::with_id("a".to_string(), "Provider A".to_string(), json!({}), None);
db.save_provider("claude", &provider_a).unwrap();
db.add_to_failover_queue("claude", "a").unwrap();
// 启用自动故障转移
let mut config = db.get_proxy_config_for_app("claude").await.unwrap();
config.auto_failover_enabled = true;
db.update_proxy_config_for_app(config).await.unwrap();
let router = ProviderRouter::new(db.clone());
// 触发熔断:1 次失败
router
.record_result("a", "claude", false, false, Some("fail".to_string()))
.await
.unwrap();
// 第一次请求:获取 HalfOpen 探测名额
let first = router.allow_provider_request("a", "claude").await;
assert!(first.allowed);
assert!(first.used_half_open_permit);
// 第二次请求应被拒绝(名额已被占用)
let second = router.allow_provider_request("a", "claude").await;
assert!(!second.allowed);
// 使用 release_permit_neutral 释放名额(不影响健康统计)
router
.release_permit_neutral("a", "claude", first.used_half_open_permit)
.await;
// 第三次请求应被允许(名额已释放)
let third = router.allow_provider_request("a", "claude").await;
assert!(third.allowed);
assert!(third.used_half_open_permit);
}
}