mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-03-22 15:08:22 +08:00
fix(proxy): thinking rectifiers
This commit is contained in:
@@ -168,7 +168,7 @@ impl Database {
|
||||
|
||||
/// 获取整流器配置
|
||||
///
|
||||
/// 返回整流器配置,如果不存在则返回默认值(全部关闭)
|
||||
/// 返回整流器配置,如果不存在则返回默认值(全部开启)
|
||||
pub fn get_rectifier_config(&self) -> Result<crate::proxy::types::RectifierConfig, AppError> {
|
||||
match self.get_setting("rectifier_config")? {
|
||||
Some(json) => serde_json::from_str(&json)
|
||||
|
||||
@@ -262,6 +262,7 @@ impl RequestForwarder {
|
||||
provider_type,
|
||||
ProviderType::Claude | ProviderType::ClaudeAuth
|
||||
);
|
||||
let mut signature_rectifier_non_retryable_client_error = false;
|
||||
|
||||
if is_anthropic_provider {
|
||||
let error_message = extract_error_message(&e);
|
||||
@@ -300,8 +301,9 @@ impl RequestForwarder {
|
||||
// 整流未生效:继续尝试 budget 整流路径,避免误判后短路
|
||||
if !rectified.applied {
|
||||
log::warn!(
|
||||
"[{app_type_str}] [RECT-006] thinking 签名整流器触发但无可整流内容,继续检查 budget 整流路径"
|
||||
"[{app_type_str}] [RECT-006] thinking 签名整流器触发但无可整流内容,继续检查 budget;若 budget 也未命中则按客户端错误返回"
|
||||
);
|
||||
signature_rectifier_non_retryable_client_error = true;
|
||||
} else {
|
||||
log::info!(
|
||||
"[{}] [RECT-001] thinking 签名整流器触发, 移除 {} thinking blocks, {} redacted_thinking blocks, {} signature fields",
|
||||
@@ -497,11 +499,10 @@ impl RequestForwarder {
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"[{}] [RECT-010] thinking budget 整流器触发, type_changed={}, budget_changed={}, max_tokens_changed={}",
|
||||
"[{}] [RECT-010] thinking budget 整流器触发, before={:?}, after={:?}",
|
||||
app_type_str,
|
||||
budget_rectified.type_changed,
|
||||
budget_rectified.budget_changed,
|
||||
budget_rectified.max_tokens_changed
|
||||
budget_rectified.before,
|
||||
budget_rectified.after
|
||||
);
|
||||
|
||||
let _ = std::mem::replace(&mut budget_rectifier_retried, true);
|
||||
@@ -616,6 +617,28 @@ impl RequestForwarder {
|
||||
}
|
||||
}
|
||||
|
||||
if signature_rectifier_non_retryable_client_error {
|
||||
self.router
|
||||
.release_permit_neutral(
|
||||
&provider.id,
|
||||
app_type_str,
|
||||
used_half_open_permit,
|
||||
)
|
||||
.await;
|
||||
let mut status = self.status.write().await;
|
||||
status.failed_requests += 1;
|
||||
status.last_error = Some(e.to_string());
|
||||
if status.total_requests > 0 {
|
||||
status.success_rate = (status.success_requests as f32
|
||||
/ status.total_requests as f32)
|
||||
* 100.0;
|
||||
}
|
||||
return Err(ForwardError {
|
||||
error: e,
|
||||
provider: Some(provider.clone()),
|
||||
});
|
||||
}
|
||||
|
||||
// 失败:记录失败并更新熔断器
|
||||
let _ = self
|
||||
.router
|
||||
|
||||
@@ -16,16 +16,25 @@ const MAX_TOKENS_VALUE: u64 = 64000;
|
||||
const MIN_MAX_TOKENS_FOR_BUDGET: u64 = MAX_THINKING_BUDGET + 1;
|
||||
|
||||
/// Budget 整流结果
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct BudgetRectifySnapshot {
|
||||
/// max_tokens
|
||||
pub max_tokens: Option<u64>,
|
||||
/// thinking.type
|
||||
pub thinking_type: Option<String>,
|
||||
/// thinking.budget_tokens
|
||||
pub thinking_budget_tokens: Option<u64>,
|
||||
}
|
||||
|
||||
/// Budget 整流结果
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct BudgetRectifyResult {
|
||||
/// 是否应用了整流
|
||||
pub applied: bool,
|
||||
/// 是否修改了 thinking type
|
||||
pub type_changed: bool,
|
||||
/// 是否修改了 budget_tokens
|
||||
pub budget_changed: bool,
|
||||
/// 是否修改了 max_tokens
|
||||
pub max_tokens_changed: bool,
|
||||
/// 整流前快照
|
||||
pub before: BudgetRectifySnapshot,
|
||||
/// 整流后快照
|
||||
pub after: BudgetRectifySnapshot,
|
||||
}
|
||||
|
||||
/// 检测是否需要触发 thinking budget 整流器
|
||||
@@ -49,27 +58,14 @@ pub fn should_rectify_thinking_budget(
|
||||
};
|
||||
let lower = msg.to_lowercase();
|
||||
|
||||
// 覆盖常见上游文案变体:
|
||||
// - budget_tokens >= 1024 约束
|
||||
// - budget_tokens 与 max_tokens 关系约束
|
||||
// 与 CCH 对齐:仅在包含 budget_tokens + thinking + 1024 约束时触发
|
||||
let has_budget_tokens_reference =
|
||||
lower.contains("budget_tokens") || lower.contains("budget tokens");
|
||||
let has_thinking_reference = lower.contains("thinking");
|
||||
let has_1024_constraint = lower.contains("greater than or equal to 1024")
|
||||
|| lower.contains(">= 1024")
|
||||
|| lower.contains("at least 1024")
|
||||
|| (lower.contains("1024") && lower.contains("input should be"));
|
||||
let has_max_tokens_constraint = lower.contains("less than max_tokens")
|
||||
|| (lower.contains("budget_tokens")
|
||||
&& lower.contains("max_tokens")
|
||||
&& (lower.contains("must be less than") || lower.contains("should be less than")));
|
||||
let has_thinking_reference = lower.contains("thinking");
|
||||
|
||||
if has_budget_tokens_reference && (has_1024_constraint || has_max_tokens_constraint) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 兜底:部分网关会省略 budget_tokens 字段名,但保留 thinking + 1024 线索
|
||||
if has_thinking_reference && has_1024_constraint {
|
||||
if has_budget_tokens_reference && has_thinking_reference && has_1024_constraint {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -83,51 +79,63 @@ pub fn should_rectify_thinking_budget(
|
||||
/// - `thinking.budget_tokens = 32000`
|
||||
/// - 如果 `max_tokens < 32001`,设为 `64000`
|
||||
pub fn rectify_thinking_budget(body: &mut Value) -> BudgetRectifyResult {
|
||||
let mut result = BudgetRectifyResult::default();
|
||||
let before = snapshot_budget(body);
|
||||
|
||||
// 仅允许对显式 thinking.type=enabled 的请求做 budget 整流,避免静默语义升级。
|
||||
let Some(thinking_obj) = body.get("thinking").and_then(|t| t.as_object()) else {
|
||||
log::warn!("[RECT-BUD-001] budget 整流命中但请求缺少 thinking 对象,跳过");
|
||||
return result;
|
||||
};
|
||||
let current_type = thinking_obj.get("type").and_then(|t| t.as_str());
|
||||
if current_type == Some("adaptive") {
|
||||
log::warn!("[RECT-BUD-002] budget 整流命中但 thinking.type=adaptive,跳过");
|
||||
return result;
|
||||
// 与 CCH 对齐:adaptive 请求不改写
|
||||
if before.thinking_type.as_deref() == Some("adaptive") {
|
||||
return BudgetRectifyResult {
|
||||
applied: false,
|
||||
before: before.clone(),
|
||||
after: before,
|
||||
};
|
||||
}
|
||||
if current_type != Some("enabled") {
|
||||
log::warn!(
|
||||
"[RECT-BUD-003] budget 整流命中但 thinking.type 不是 enabled(当前: {}),跳过",
|
||||
current_type.unwrap_or("<missing>")
|
||||
);
|
||||
return result;
|
||||
|
||||
// 与 CCH 对齐:缺少/非法 thinking 时自动创建后再整流
|
||||
if !body.get("thinking").is_some_and(Value::is_object) {
|
||||
body["thinking"] = Value::Object(serde_json::Map::new());
|
||||
}
|
||||
|
||||
let Some(thinking) = body.get_mut("thinking").and_then(|t| t.as_object_mut()) else {
|
||||
return result;
|
||||
return BudgetRectifyResult {
|
||||
applied: false,
|
||||
before: before.clone(),
|
||||
after: before,
|
||||
};
|
||||
};
|
||||
|
||||
// 设置 budget_tokens = MAX_THINKING_BUDGET
|
||||
let current_budget = thinking.get("budget_tokens").and_then(|v| v.as_u64());
|
||||
if current_budget != Some(MAX_THINKING_BUDGET) {
|
||||
thinking.insert(
|
||||
"budget_tokens".to_string(),
|
||||
Value::Number(MAX_THINKING_BUDGET.into()),
|
||||
);
|
||||
result.budget_changed = true;
|
||||
}
|
||||
thinking.insert("type".to_string(), Value::String("enabled".to_string()));
|
||||
thinking.insert(
|
||||
"budget_tokens".to_string(),
|
||||
Value::Number(MAX_THINKING_BUDGET.into()),
|
||||
);
|
||||
|
||||
// 确保 max_tokens >= MIN_MAX_TOKENS_FOR_BUDGET
|
||||
let current_max_tokens = body.get("max_tokens").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
if current_max_tokens < MIN_MAX_TOKENS_FOR_BUDGET {
|
||||
if before.max_tokens.is_none() || before.max_tokens < Some(MIN_MAX_TOKENS_FOR_BUDGET) {
|
||||
body["max_tokens"] = Value::Number(MAX_TOKENS_VALUE.into());
|
||||
result.max_tokens_changed = true;
|
||||
}
|
||||
|
||||
result.applied = result.type_changed || result.budget_changed || result.max_tokens_changed;
|
||||
if !result.applied {
|
||||
log::warn!("[RECT-BUD-004] budget 整流命中但请求已满足约束,跳过重试");
|
||||
let after = snapshot_budget(body);
|
||||
BudgetRectifyResult {
|
||||
applied: before != after,
|
||||
before,
|
||||
after,
|
||||
}
|
||||
}
|
||||
|
||||
fn snapshot_budget(body: &Value) -> BudgetRectifySnapshot {
|
||||
let max_tokens = body.get("max_tokens").and_then(|v| v.as_u64());
|
||||
let thinking = body.get("thinking").and_then(|t| t.as_object());
|
||||
let thinking_type = thinking
|
||||
.and_then(|t| t.get("type"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(ToString::to_string);
|
||||
let thinking_budget_tokens = thinking
|
||||
.and_then(|t| t.get("budget_tokens"))
|
||||
.and_then(|v| v.as_u64());
|
||||
BudgetRectifySnapshot {
|
||||
max_tokens,
|
||||
thinking_type,
|
||||
thinking_budget_tokens,
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -171,7 +179,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_detect_budget_tokens_max_tokens_error() {
|
||||
assert!(should_rectify_thinking_budget(
|
||||
assert!(!should_rectify_thinking_budget(
|
||||
Some("budget_tokens must be less than max_tokens"),
|
||||
&enabled_config()
|
||||
));
|
||||
@@ -179,7 +187,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_detect_budget_tokens_1024_error() {
|
||||
assert!(should_rectify_thinking_budget(
|
||||
assert!(!should_rectify_thinking_budget(
|
||||
Some("budget_tokens: value must be at least 1024"),
|
||||
&enabled_config()
|
||||
));
|
||||
@@ -231,8 +239,15 @@ mod tests {
|
||||
let result = rectify_thinking_budget(&mut body);
|
||||
|
||||
assert!(result.applied);
|
||||
assert!(result.budget_changed);
|
||||
assert!(result.max_tokens_changed);
|
||||
assert_eq!(result.before.thinking_type.as_deref(), Some("enabled"));
|
||||
assert_eq!(result.after.thinking_type.as_deref(), Some("enabled"));
|
||||
assert_eq!(result.before.thinking_budget_tokens, Some(512));
|
||||
assert_eq!(
|
||||
result.after.thinking_budget_tokens,
|
||||
Some(MAX_THINKING_BUDGET)
|
||||
);
|
||||
assert_eq!(result.before.max_tokens, Some(1024));
|
||||
assert_eq!(result.after.max_tokens, Some(MAX_TOKENS_VALUE));
|
||||
assert_eq!(body["thinking"]["type"], "enabled");
|
||||
assert_eq!(body["thinking"]["budget_tokens"], MAX_THINKING_BUDGET);
|
||||
assert_eq!(body["max_tokens"], MAX_TOKENS_VALUE);
|
||||
@@ -249,7 +264,7 @@ mod tests {
|
||||
let result = rectify_thinking_budget(&mut body);
|
||||
|
||||
assert!(!result.applied);
|
||||
assert!(!result.type_changed);
|
||||
assert_eq!(result.before, result.after);
|
||||
assert_eq!(body["thinking"]["type"], "adaptive");
|
||||
assert_eq!(body["thinking"]["budget_tokens"], 512);
|
||||
assert_eq!(body["max_tokens"], 1024);
|
||||
@@ -266,7 +281,8 @@ mod tests {
|
||||
let result = rectify_thinking_budget(&mut body);
|
||||
|
||||
assert!(result.applied);
|
||||
assert!(!result.max_tokens_changed);
|
||||
assert_eq!(result.before.max_tokens, Some(100000));
|
||||
assert_eq!(result.after.max_tokens, Some(100000));
|
||||
assert_eq!(body["max_tokens"], 100000);
|
||||
}
|
||||
|
||||
@@ -279,9 +295,17 @@ mod tests {
|
||||
|
||||
let result = rectify_thinking_budget(&mut body);
|
||||
|
||||
assert!(!result.applied);
|
||||
assert!(body.get("thinking").is_none());
|
||||
assert_eq!(body["max_tokens"], 1024);
|
||||
assert!(result.applied);
|
||||
assert_eq!(result.before.thinking_type, None);
|
||||
assert_eq!(result.after.thinking_type.as_deref(), Some("enabled"));
|
||||
assert_eq!(
|
||||
result.after.thinking_budget_tokens,
|
||||
Some(MAX_THINKING_BUDGET)
|
||||
);
|
||||
assert_eq!(result.after.max_tokens, Some(MAX_TOKENS_VALUE));
|
||||
assert_eq!(body["thinking"]["type"], "enabled");
|
||||
assert_eq!(body["thinking"]["budget_tokens"], MAX_THINKING_BUDGET);
|
||||
assert_eq!(body["max_tokens"], MAX_TOKENS_VALUE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -294,12 +318,13 @@ mod tests {
|
||||
let result = rectify_thinking_budget(&mut body);
|
||||
|
||||
assert!(result.applied);
|
||||
assert!(result.max_tokens_changed);
|
||||
assert_eq!(result.before.max_tokens, None);
|
||||
assert_eq!(result.after.max_tokens, Some(MAX_TOKENS_VALUE));
|
||||
assert_eq!(body["max_tokens"], MAX_TOKENS_VALUE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rectify_budget_skips_non_enabled_type() {
|
||||
fn test_rectify_budget_normalizes_non_enabled_type() {
|
||||
let mut body = json!({
|
||||
"model": "claude-test",
|
||||
"thinking": { "type": "disabled", "budget_tokens": 512 },
|
||||
@@ -308,10 +333,12 @@ mod tests {
|
||||
|
||||
let result = rectify_thinking_budget(&mut body);
|
||||
|
||||
assert!(!result.applied);
|
||||
assert_eq!(body["thinking"]["type"], "disabled");
|
||||
assert_eq!(body["thinking"]["budget_tokens"], 512);
|
||||
assert_eq!(body["max_tokens"], 1024);
|
||||
assert!(result.applied);
|
||||
assert_eq!(result.before.thinking_type.as_deref(), Some("disabled"));
|
||||
assert_eq!(result.after.thinking_type.as_deref(), Some("enabled"));
|
||||
assert_eq!(body["thinking"]["type"], "enabled");
|
||||
assert_eq!(body["thinking"]["budget_tokens"], MAX_THINKING_BUDGET);
|
||||
assert_eq!(body["max_tokens"], MAX_TOKENS_VALUE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -325,8 +352,7 @@ mod tests {
|
||||
let result = rectify_thinking_budget(&mut body);
|
||||
|
||||
assert!(!result.applied);
|
||||
assert!(!result.budget_changed);
|
||||
assert!(!result.max_tokens_changed);
|
||||
assert_eq!(result.before, result.after);
|
||||
assert_eq!(body["thinking"]["budget_tokens"], 32000);
|
||||
assert_eq!(body["max_tokens"], 64001);
|
||||
}
|
||||
|
||||
@@ -89,15 +89,11 @@ pub fn should_rectify_thinking_signature(
|
||||
return true;
|
||||
}
|
||||
|
||||
// 场景7: 非法请求(需携带签名/思考结构线索,避免过宽命中)
|
||||
let invalid_request_like = lower.contains("非法请求")
|
||||
// 场景7: 非法请求(与 CCH 对齐,按 invalid request 统一兜底)
|
||||
if lower.contains("非法请求")
|
||||
|| lower.contains("illegal request")
|
||||
|| lower.contains("invalid request");
|
||||
let has_signature_or_thinking_clue = lower.contains("signature")
|
||||
|| lower.contains("thinking")
|
||||
|| lower.contains("redacted_thinking")
|
||||
|| lower.contains("tool_use");
|
||||
if invalid_request_like && has_signature_or_thinking_clue {
|
||||
|| lower.contains("invalid request")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -114,15 +110,6 @@ pub fn should_rectify_thinking_signature(
|
||||
pub fn rectify_anthropic_request(body: &mut Value) -> RectifyResult {
|
||||
let mut result = RectifyResult::default();
|
||||
|
||||
// 与 CCH 对齐:adaptive 模式下整流器直接跳过,不改写请求。
|
||||
let thinking_type = body
|
||||
.get("thinking")
|
||||
.and_then(|t| t.get("type"))
|
||||
.and_then(|t| t.as_str());
|
||||
if thinking_type == Some("adaptive") {
|
||||
return result;
|
||||
}
|
||||
|
||||
let messages = match body.get_mut("messages").and_then(|m| m.as_array_mut()) {
|
||||
Some(m) => m,
|
||||
None => return result,
|
||||
@@ -496,7 +483,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_detect_invalid_request() {
|
||||
// 场景7: 非法请求(包含签名/思考结构线索才触发)
|
||||
// 场景7: 非法请求(与 CCH 对齐,统一触发)
|
||||
assert!(should_rectify_thinking_signature(
|
||||
Some("非法请求:thinking signature 不合法"),
|
||||
&enabled_config()
|
||||
@@ -505,7 +492,7 @@ mod tests {
|
||||
Some("illegal request: tool_use block mismatch"),
|
||||
&enabled_config()
|
||||
));
|
||||
assert!(!should_rectify_thinking_signature(
|
||||
assert!(should_rectify_thinking_signature(
|
||||
Some("invalid request: malformed JSON"),
|
||||
&enabled_config()
|
||||
));
|
||||
@@ -523,7 +510,7 @@ mod tests {
|
||||
// ==================== adaptive thinking type 测试 ====================
|
||||
|
||||
#[test]
|
||||
fn test_rectify_skips_when_adaptive_thinking_type() {
|
||||
fn test_rectify_keeps_adaptive_when_no_legacy_blocks() {
|
||||
let mut body = json!({
|
||||
"model": "claude-test",
|
||||
"thinking": { "type": "adaptive" },
|
||||
@@ -577,7 +564,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_rectify_removes_top_level_thinking_adaptive() {
|
||||
// 与 CCH 对齐:adaptive 模式下整流器直接跳过,不删除顶层 thinking
|
||||
// 顶层 thinking 仅在 type=enabled 且 tool_use 场景才会删除,adaptive 不删除
|
||||
let mut body = json!({
|
||||
"model": "claude-test",
|
||||
"thinking": { "type": "adaptive" },
|
||||
@@ -598,6 +585,31 @@ mod tests {
|
||||
assert_eq!(body["thinking"]["type"], "adaptive");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rectify_adaptive_still_cleans_legacy_signature_blocks() {
|
||||
let mut body = json!({
|
||||
"model": "claude-test",
|
||||
"thinking": { "type": "adaptive" },
|
||||
"messages": [{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{ "type": "thinking", "thinking": "t", "signature": "sig_thinking" },
|
||||
{ "type": "text", "text": "hello", "signature": "sig_text" }
|
||||
]
|
||||
}]
|
||||
});
|
||||
|
||||
let result = rectify_anthropic_request(&mut body);
|
||||
|
||||
assert!(result.applied);
|
||||
assert_eq!(result.removed_thinking_blocks, 1);
|
||||
let content = body["messages"][0]["content"].as_array().unwrap();
|
||||
assert_eq!(content.len(), 1);
|
||||
assert_eq!(content[0]["type"], "text");
|
||||
assert!(content[0].get("signature").is_none());
|
||||
assert_eq!(body["thinking"]["type"], "adaptive");
|
||||
}
|
||||
|
||||
// ==================== normalize_thinking_type 测试 ====================
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -195,28 +195,24 @@ pub struct AppProxyConfig {
|
||||
/// 整流器配置
|
||||
///
|
||||
/// 存储在 settings 表中
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RectifierConfig {
|
||||
/// 总开关:是否启用整流器(默认关闭)
|
||||
#[serde(default = "default_false")]
|
||||
/// 总开关:是否启用整流器(默认开启)
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
/// 请求整流:启用 thinking 签名整流器(默认关闭)
|
||||
/// 请求整流:启用 thinking 签名整流器(默认开启)
|
||||
///
|
||||
/// 处理错误:Invalid 'signature' in 'thinking' block
|
||||
#[serde(default = "default_false")]
|
||||
#[serde(default = "default_true")]
|
||||
pub request_thinking_signature: bool,
|
||||
/// 请求整流:启用 thinking budget 整流器(默认关闭)
|
||||
/// 请求整流:启用 thinking budget 整流器(默认开启)
|
||||
///
|
||||
/// 处理错误:budget_tokens + thinking 相关约束
|
||||
#[serde(default = "default_false")]
|
||||
#[serde(default = "default_true")]
|
||||
pub request_thinking_budget: bool,
|
||||
}
|
||||
|
||||
fn default_false() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
@@ -225,6 +221,16 @@ fn default_log_level() -> String {
|
||||
"info".to_string()
|
||||
}
|
||||
|
||||
impl Default for RectifierConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
request_thinking_signature: true,
|
||||
request_thinking_budget: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 日志配置
|
||||
///
|
||||
/// 存储在 settings 表的 log_config 字段中(JSON 格式)
|
||||
@@ -270,28 +276,28 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_rectifier_config_default_disabled() {
|
||||
// 验证 RectifierConfig::default() 返回全关闭状态
|
||||
fn test_rectifier_config_default_enabled() {
|
||||
// 验证 RectifierConfig::default() 返回全开启状态
|
||||
let config = RectifierConfig::default();
|
||||
assert!(!config.enabled, "整流器总开关默认应为 false");
|
||||
assert!(config.enabled, "整流器总开关默认应为 true");
|
||||
assert!(
|
||||
!config.request_thinking_signature,
|
||||
"thinking 签名整流器默认应为 false"
|
||||
config.request_thinking_signature,
|
||||
"thinking 签名整流器默认应为 true"
|
||||
);
|
||||
assert!(
|
||||
!config.request_thinking_budget,
|
||||
"thinking budget 整流器默认应为 false"
|
||||
config.request_thinking_budget,
|
||||
"thinking budget 整流器默认应为 true"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rectifier_config_serde_default() {
|
||||
// 验证反序列化缺字段时使用默认值 false
|
||||
// 验证反序列化缺字段时使用默认值 true
|
||||
let json = "{}";
|
||||
let config: RectifierConfig = serde_json::from_str(json).unwrap();
|
||||
assert!(!config.enabled);
|
||||
assert!(!config.request_thinking_signature);
|
||||
assert!(!config.request_thinking_budget);
|
||||
assert!(config.enabled);
|
||||
assert!(config.request_thinking_signature);
|
||||
assert!(config.request_thinking_budget);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -307,12 +313,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_rectifier_config_serde_partial_fields() {
|
||||
// 验证只设置部分字段时,缺失字段使用默认值 false
|
||||
// 验证只设置部分字段时,缺失字段使用默认值 true
|
||||
let json = r#"{"enabled": true, "requestThinkingSignature": false}"#;
|
||||
let config: RectifierConfig = serde_json::from_str(json).unwrap();
|
||||
assert!(config.enabled);
|
||||
assert!(!config.request_thinking_signature);
|
||||
assert!(!config.request_thinking_budget);
|
||||
assert!(config.request_thinking_budget);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -141,7 +141,11 @@ function App() {
|
||||
|
||||
// Fallback from sessions view when switching to an app without session support
|
||||
useEffect(() => {
|
||||
if (currentView === "sessions" && activeApp !== "claude" && activeApp !== "codex") {
|
||||
if (
|
||||
currentView === "sessions" &&
|
||||
activeApp !== "claude" &&
|
||||
activeApp !== "codex"
|
||||
) {
|
||||
setCurrentView("providers");
|
||||
}
|
||||
}, [activeApp, currentView]);
|
||||
|
||||
@@ -8,9 +8,9 @@ import { settingsApi, type RectifierConfig } from "@/lib/api/settings";
|
||||
export function RectifierConfigPanel() {
|
||||
const { t } = useTranslation();
|
||||
const [config, setConfig] = useState<RectifierConfig>({
|
||||
enabled: false,
|
||||
requestThinkingSignature: false,
|
||||
requestThinkingBudget: false,
|
||||
enabled: true,
|
||||
requestThinkingSignature: true,
|
||||
requestThinkingBudget: true,
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@
|
||||
"thinkingSignature": "Thinking Signature Rectification",
|
||||
"thinkingSignatureDescription": "When an Anthropic-type provider returns thinking signature incompatibility or illegal request errors, automatically removes incompatible thinking-related blocks and retries once with the same provider",
|
||||
"thinkingBudget": "Thinking Budget Rectification",
|
||||
"thinkingBudgetDescription": "When an Anthropic-type provider returns budget_tokens constraint errors (such as at least 1024 or less than max_tokens), and the request explicitly enables thinking (type=enabled), automatically sets thinking budget to 32000 and max_tokens to 64000 if needed, then retries once"
|
||||
"thinkingBudgetDescription": "When an Anthropic-type provider returns budget_tokens constraint errors (such as at least 1024), automatically normalizes thinking to enabled, sets thinking budget to 32000, and raises max_tokens to 64000 if needed, then retries once"
|
||||
},
|
||||
"logConfig": {
|
||||
"title": "Log Management",
|
||||
|
||||
@@ -216,7 +216,7 @@
|
||||
"thinkingSignature": "Thinking 署名整流",
|
||||
"thinkingSignatureDescription": "Anthropic タイプのプロバイダーが thinking 署名の非互換性や不正なリクエストエラーを返した場合、互換性のない thinking 関連ブロックを自動削除し、同じプロバイダーで 1 回リトライします",
|
||||
"thinkingBudget": "Thinking Budget 整流",
|
||||
"thinkingBudgetDescription": "Anthropic タイプのプロバイダーが budget_tokens 制約エラー(例: 1024 以上、max_tokens 未満)を返し、かつリクエストで thinking(type=enabled)が明示的に有効な場合、thinking 予算を 32000 に設定し、必要に応じて max_tokens を 64000 に設定して 1 回リトライします"
|
||||
"thinkingBudgetDescription": "Anthropic タイプのプロバイダーが budget_tokens 制約エラー(例: 1024 以上)を返した場合、thinking を enabled に正規化し、thinking 予算を 32000 に設定し、必要に応じて max_tokens を 64000 に引き上げて 1 回リトライします"
|
||||
},
|
||||
"logConfig": {
|
||||
"title": "ログ管理",
|
||||
|
||||
@@ -216,7 +216,7 @@
|
||||
"thinkingSignature": "Thinking 签名整流",
|
||||
"thinkingSignatureDescription": "当 Anthropic 类型供应商返回 thinking 签名不兼容或非法请求等错误时,自动移除不兼容的 thinking 相关块并对同一供应商重试一次",
|
||||
"thinkingBudget": "Thinking Budget 整流",
|
||||
"thinkingBudgetDescription": "当 Anthropic 类型供应商返回 budget_tokens 约束错误(如至少 1024、需小于 max_tokens)时,在请求显式开启 thinking(type=enabled)时自动将预算设为 32000,并在需要时将 max_tokens 设为 64000,然后重试一次"
|
||||
"thinkingBudgetDescription": "当 Anthropic 类型供应商返回 budget_tokens 约束错误(如至少 1024)时,自动将 thinking 规范为 enabled 并将 budget 设为 32000,同时在需要时将 max_tokens 设为 64000,然后重试一次"
|
||||
},
|
||||
"logConfig": {
|
||||
"title": "日志管理",
|
||||
|
||||
Reference in New Issue
Block a user