mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-29 01:25:33 +08:00
feat: add Bedrock request optimizer (PRE-SEND thinking + cache injection) (#1301)
* feat: add Bedrock request optimizer (PRE-SEND thinking + cache injection) Add a PRE-SEND request optimizer that enhances Bedrock API requests before forwarding, complementing the existing POST-ERROR rectifier system. New modules: - thinking_optimizer: 3-path model detection (adaptive/legacy/skip) - Opus 4.6/Sonnet 4.6: adaptive thinking + effort max + 1M context beta - Legacy models: inject extended thinking with max budget - Haiku: skip (no modification) - cache_injector: auto-inject cache_control breakpoints (max 4) - Injects at tools/system/assistant message positions - TTL upgrade for existing breakpoints (5m → 1h) Gate: only activates for Bedrock providers (CLAUDE_CODE_USE_BEDROCK=1) Config: stored in SQLite settings table, default OFF, user opt-in UI: new Optimizer section in RectifierConfigPanel with 3 toggles + TTL 18 unit tests covering all paths. Verified against live Bedrock API. * chore: remove docs/plans directory * fix: address code review findings for Bedrock request optimizer P0 fixes: - Replace hardcoded Chinese with i18n t() calls in optimizer panel, add translation keys to zh/en/ja locale files - Fix u64 underflow: max_tokens - 1 → max_tokens.saturating_sub(1) - Move optimizer from before retry loop to per-provider with body cloning, preventing Bedrock fields leaking to non-Bedrock providers P1 fixes: - Replace .map() side-effect pattern with idiomatic if-let (clippy) - Fix module alphabetical ordering in mod.rs - Add cache_ttl whitelist validation in set_optimizer_config - Remove #[allow(unused_assignments)] and dead budget decrement --------- Co-authored-by: Keith (via OpenClaw) <keithyt06@users.noreply.github.com> Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
//! Thinking 优化器
|
||||
|
||||
use super::types::OptimizerConfig;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
/// 根据模型类型自动优化 thinking 配置
|
||||
///
|
||||
/// 三路径分发:
|
||||
/// - skip: haiku 模型直接跳过
|
||||
/// - adaptive: opus-4-6 / sonnet-4-6 使用 adaptive thinking
|
||||
/// - legacy: 其他模型注入 enabled thinking + budget_tokens
|
||||
pub fn optimize(body: &mut Value, config: &OptimizerConfig) {
|
||||
if !config.thinking_optimizer {
|
||||
return;
|
||||
}
|
||||
|
||||
let model = match body.get("model").and_then(|m| m.as_str()) {
|
||||
Some(m) => m.to_lowercase(),
|
||||
None => return,
|
||||
};
|
||||
|
||||
if model.contains("haiku") {
|
||||
log::info!("[OPT] thinking: skip(haiku)");
|
||||
return;
|
||||
}
|
||||
|
||||
if model.contains("opus-4-6") || model.contains("sonnet-4-6") {
|
||||
log::info!("[OPT] thinking: adaptive({})", model);
|
||||
body["thinking"] = json!({"type": "adaptive"});
|
||||
body["output_config"] = json!({"effort": "max"});
|
||||
append_beta(body, "context-1m-2025-08-07");
|
||||
return;
|
||||
}
|
||||
|
||||
// legacy path
|
||||
log::info!("[OPT] thinking: legacy({})", model);
|
||||
|
||||
let max_tokens = body
|
||||
.get("max_tokens")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(16384);
|
||||
|
||||
let budget_target = max_tokens.saturating_sub(1);
|
||||
|
||||
let thinking_type = body
|
||||
.get("thinking")
|
||||
.and_then(|t| t.get("type"))
|
||||
.and_then(|t| t.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
match thinking_type.as_deref() {
|
||||
None | Some("disabled") => {
|
||||
body["thinking"] = json!({
|
||||
"type": "enabled",
|
||||
"budget_tokens": budget_target
|
||||
});
|
||||
append_beta(body, "interleaved-thinking-2025-05-14");
|
||||
}
|
||||
Some("enabled") => {
|
||||
let current_budget = body
|
||||
.get("thinking")
|
||||
.and_then(|t| t.get("budget_tokens"))
|
||||
.and_then(|b| b.as_u64())
|
||||
.unwrap_or(0);
|
||||
if current_budget < budget_target {
|
||||
body["thinking"]["budget_tokens"] = json!(budget_target);
|
||||
}
|
||||
append_beta(body, "interleaved-thinking-2025-05-14");
|
||||
}
|
||||
_ => {
|
||||
append_beta(body, "interleaved-thinking-2025-05-14");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 追加 beta 标识到 anthropic_beta 数组(去重)
|
||||
fn append_beta(body: &mut Value, beta: &str) {
|
||||
match body.get("anthropic_beta") {
|
||||
Some(Value::Array(arr)) => {
|
||||
if arr.iter().any(|v| v.as_str() == Some(beta)) {
|
||||
return;
|
||||
}
|
||||
body["anthropic_beta"]
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
.push(json!(beta));
|
||||
}
|
||||
Some(Value::Null) | None => {
|
||||
body["anthropic_beta"] = json!([beta]);
|
||||
}
|
||||
_ => {
|
||||
body["anthropic_beta"] = json!([beta]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
fn enabled_config() -> OptimizerConfig {
|
||||
OptimizerConfig {
|
||||
enabled: true,
|
||||
thinking_optimizer: true,
|
||||
cache_injection: true,
|
||||
cache_ttl: "1h".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn disabled_config() -> OptimizerConfig {
|
||||
OptimizerConfig {
|
||||
enabled: true,
|
||||
thinking_optimizer: false,
|
||||
cache_injection: true,
|
||||
cache_ttl: "1h".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adaptive_opus_4_6() {
|
||||
let mut body = json!({
|
||||
"model": "anthropic.claude-opus-4-6-20250514-v1:0",
|
||||
"max_tokens": 16384,
|
||||
"thinking": {"type": "enabled", "budget_tokens": 8000},
|
||||
"messages": [{"role": "user", "content": "hello"}]
|
||||
});
|
||||
|
||||
optimize(&mut body, &enabled_config());
|
||||
|
||||
assert_eq!(body["thinking"]["type"], "adaptive");
|
||||
assert!(body["thinking"].get("budget_tokens").is_none());
|
||||
assert_eq!(body["output_config"]["effort"], "max");
|
||||
let betas = body["anthropic_beta"].as_array().unwrap();
|
||||
assert!(betas.iter().any(|v| v == "context-1m-2025-08-07"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adaptive_sonnet_4_6() {
|
||||
let mut body = json!({
|
||||
"model": "anthropic.claude-sonnet-4-6-20250514-v1:0",
|
||||
"max_tokens": 16384,
|
||||
"messages": [{"role": "user", "content": "hello"}]
|
||||
});
|
||||
|
||||
optimize(&mut body, &enabled_config());
|
||||
|
||||
assert_eq!(body["thinking"]["type"], "adaptive");
|
||||
assert!(body["thinking"].get("budget_tokens").is_none());
|
||||
assert_eq!(body["output_config"]["effort"], "max");
|
||||
let betas = body["anthropic_beta"].as_array().unwrap();
|
||||
assert!(betas.iter().any(|v| v == "context-1m-2025-08-07"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_legacy_sonnet_4_5_thinking_null() {
|
||||
let mut body = json!({
|
||||
"model": "anthropic.claude-sonnet-4-5-20250514-v1:0",
|
||||
"max_tokens": 16384,
|
||||
"messages": [{"role": "user", "content": "hello"}]
|
||||
});
|
||||
|
||||
optimize(&mut body, &enabled_config());
|
||||
|
||||
assert_eq!(body["thinking"]["type"], "enabled");
|
||||
assert_eq!(body["thinking"]["budget_tokens"], 16383);
|
||||
let betas = body["anthropic_beta"].as_array().unwrap();
|
||||
assert!(betas.iter().any(|v| v == "interleaved-thinking-2025-05-14"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_legacy_budget_too_small_upgraded() {
|
||||
let mut body = json!({
|
||||
"model": "anthropic.claude-sonnet-4-5-20250514-v1:0",
|
||||
"max_tokens": 16384,
|
||||
"thinking": {"type": "enabled", "budget_tokens": 1024},
|
||||
"messages": [{"role": "user", "content": "hello"}]
|
||||
});
|
||||
|
||||
optimize(&mut body, &enabled_config());
|
||||
|
||||
assert_eq!(body["thinking"]["type"], "enabled");
|
||||
assert_eq!(body["thinking"]["budget_tokens"], 16383);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_skip_haiku() {
|
||||
let mut body = json!({
|
||||
"model": "anthropic.claude-haiku-4-5-20250514-v1:0",
|
||||
"max_tokens": 8192,
|
||||
"messages": [{"role": "user", "content": "hello"}]
|
||||
});
|
||||
let original = body.clone();
|
||||
|
||||
optimize(&mut body, &enabled_config());
|
||||
|
||||
assert_eq!(body, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_thinking_optimizer_disabled() {
|
||||
let mut body = json!({
|
||||
"model": "anthropic.claude-opus-4-6-20250514-v1:0",
|
||||
"max_tokens": 16384,
|
||||
"messages": [{"role": "user", "content": "hello"}]
|
||||
});
|
||||
let original = body.clone();
|
||||
|
||||
optimize(&mut body, &disabled_config());
|
||||
|
||||
assert_eq!(body, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adaptive_dedup_beta() {
|
||||
let mut body = json!({
|
||||
"model": "anthropic.claude-opus-4-6-20250514-v1:0",
|
||||
"max_tokens": 16384,
|
||||
"anthropic_beta": ["context-1m-2025-08-07"],
|
||||
"messages": [{"role": "user", "content": "hello"}]
|
||||
});
|
||||
|
||||
optimize(&mut body, &enabled_config());
|
||||
|
||||
let betas = body["anthropic_beta"].as_array().unwrap();
|
||||
let count = betas
|
||||
.iter()
|
||||
.filter(|v| v == &&json!("context-1m-2025-08-07"))
|
||||
.count();
|
||||
assert_eq!(count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_legacy_disabled_thinking_injected() {
|
||||
let mut body = json!({
|
||||
"model": "anthropic.claude-sonnet-4-5-20250514-v1:0",
|
||||
"max_tokens": 8192,
|
||||
"thinking": {"type": "disabled"},
|
||||
"messages": [{"role": "user", "content": "hello"}]
|
||||
});
|
||||
|
||||
optimize(&mut body, &enabled_config());
|
||||
|
||||
assert_eq!(body["thinking"]["type"], "enabled");
|
||||
assert_eq!(body["thinking"]["budget_tokens"], 8191);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_legacy_default_max_tokens() {
|
||||
let mut body = json!({
|
||||
"model": "anthropic.claude-sonnet-4-5-20250514-v1:0",
|
||||
"messages": [{"role": "user", "content": "hello"}]
|
||||
});
|
||||
|
||||
optimize(&mut body, &enabled_config());
|
||||
|
||||
assert_eq!(body["thinking"]["type"], "enabled");
|
||||
assert_eq!(body["thinking"]["budget_tokens"], 16383);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_append_beta_null_field() {
|
||||
let mut body = json!({
|
||||
"model": "anthropic.claude-opus-4-6-20250514-v1:0",
|
||||
"anthropic_beta": null,
|
||||
"messages": [{"role": "user", "content": "hello"}]
|
||||
});
|
||||
|
||||
optimize(&mut body, &enabled_config());
|
||||
|
||||
let betas = body["anthropic_beta"].as_array().unwrap();
|
||||
assert!(betas.iter().any(|v| v == "context-1m-2025-08-07"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user