Files
cc-switch/src-tauri/src/proxy/thinking_optimizer.rs
T
Jason 83c3c3b494 feat(pricing): add Claude Opus 4.7 with adaptive thinking and Bedrock SKU
- Seed claude-opus-4-7 pricing (same tier as 4.6: $5 / $25 / $0.50 /
  $6.25 per million tokens). Relies on incremental INSERT OR IGNORE
  seeding; no SCHEMA_VERSION bump needed.
- Whitelist opus-4-7 in thinking optimizer so it uses adaptive
  thinking + max effort + 1M context beta, matching 4.6 behavior.
- Bump default OPUS model in PIPELLM and AWS Bedrock (AKSK / API Key)
  presets to 4.7. Bedrock SKU drops the -v1 suffix per the official
  4.7 model card (anthropic.claude-opus-4-7 and
  global.anthropic.claude-opus-4-7).
2026-04-21 11:57:06 +08:00

275 lines
8.3 KiB
Rust

//! Thinking 优化器
use super::types::OptimizerConfig;
use serde_json::{json, Value};
/// 根据模型类型自动优化 thinking 配置
///
/// 三路径分发:
/// - skip: haiku 模型直接跳过
/// - adaptive: opus-4-7 / 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-7") || 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"));
}
}