Files
CC-Switch/src-tauri/src/proxy/handler_context.rs
T
Keith Yu 8217bfff50 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>
2026-03-07 18:57:21 +08:00

262 lines
8.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 请求上下文模块
//!
//! 提供请求生命周期的上下文管理,封装通用初始化逻辑
use crate::app_config::AppType;
use crate::provider::Provider;
use crate::proxy::{
extract_session_id,
forwarder::RequestForwarder,
server::ProxyState,
types::{AppProxyConfig, OptimizerConfig, RectifierConfig},
ProxyError,
};
use axum::http::HeaderMap;
use std::time::Instant;
/// 流式超时配置
#[derive(Debug, Clone, Copy)]
pub struct StreamingTimeoutConfig {
/// 首字节超时(秒),0 表示禁用
pub first_byte_timeout: u64,
/// 静默期超时(秒),0 表示禁用
pub idle_timeout: u64,
}
/// 请求上下文
///
/// 贯穿整个请求生命周期,包含:
/// - 计时信息
/// - 应用级代理配置(per-app
/// - 选中的 Provider 列表(用于故障转移)
/// - 请求模型名称
/// - 日志标签
/// - Session ID(用于日志关联)
pub struct RequestContext {
/// 请求开始时间
pub start_time: Instant,
/// 应用级代理配置(per-app,包含重试次数和超时配置)
pub app_config: AppProxyConfig,
/// 选中的 Provider(故障转移链的第一个)
pub provider: Provider,
/// 完整的 Provider 列表(用于故障转移)
providers: Vec<Provider>,
/// 请求开始时的"当前供应商"(用于判断是否需要同步 UI/托盘)
///
/// 这里使用本地 settings 的设备级 current provider。
/// 代理模式下如果实际使用的 provider 与此不一致,会触发切换以确保 UI 始终准确。
pub current_provider_id: String,
/// 请求中的模型名称
pub request_model: String,
/// 日志标签(如 "Claude"、"Codex"、"Gemini"
pub tag: &'static str,
/// 应用类型字符串(如 "claude"、"codex"、"gemini"
pub app_type_str: &'static str,
/// 应用类型(预留,目前通过 app_type_str 使用)
#[allow(dead_code)]
pub app_type: AppType,
/// Session ID(从客户端请求提取或新生成)
pub session_id: String,
/// 整流器配置
pub rectifier_config: RectifierConfig,
/// 优化器配置
pub optimizer_config: OptimizerConfig,
}
impl RequestContext {
/// 创建请求上下文
///
/// # Arguments
/// * `state` - 代理服务器状态
/// * `body` - 请求体 JSON
/// * `headers` - 请求头(用于提取 Session ID
/// * `app_type` - 应用类型
/// * `tag` - 日志标签
/// * `app_type_str` - 应用类型字符串
///
/// # Errors
/// 返回 `ProxyError` 如果 Provider 选择失败
pub async fn new(
state: &ProxyState,
body: &serde_json::Value,
headers: &HeaderMap,
app_type: AppType,
tag: &'static str,
app_type_str: &'static str,
) -> Result<Self, ProxyError> {
let start_time = Instant::now();
// 从数据库读取应用级代理配置(per-app)
let app_config = state
.db
.get_proxy_config_for_app(app_type_str)
.await
.map_err(|e| ProxyError::DatabaseError(e.to_string()))?;
// 从数据库读取整流器配置
let rectifier_config = state.db.get_rectifier_config().unwrap_or_default();
let optimizer_config = state.db.get_optimizer_config().unwrap_or_default();
let current_provider_id =
crate::settings::get_current_provider(&app_type).unwrap_or_default();
// 从请求体提取模型名称
let request_model = body
.get("model")
.and_then(|m| m.as_str())
.unwrap_or("unknown")
.to_string();
// 提取 Session ID
let session_result = extract_session_id(headers, body, app_type_str);
let session_id = session_result.session_id.clone();
log::debug!(
"[{}] Session ID: {} (from {:?}, client_provided: {})",
tag,
session_id,
session_result.source,
session_result.client_provided
);
// 使用共享的 ProviderRouter 选择 Provider(熔断器状态跨请求保持)
// 注意:只在这里调用一次,结果传递给 forwarder,避免重复消耗 HalfOpen 名额
let providers = state
.provider_router
.select_providers(app_type_str)
.await
.map_err(|e| match e {
crate::error::AppError::AllProvidersCircuitOpen => {
ProxyError::AllProvidersCircuitOpen
}
crate::error::AppError::NoProvidersConfigured => ProxyError::NoProvidersConfigured,
_ => ProxyError::DatabaseError(e.to_string()),
})?;
let provider = providers
.first()
.cloned()
.ok_or(ProxyError::NoAvailableProvider)?;
log::debug!(
"[{}] Provider: {}, model: {}, failover chain: {} providers, session: {}",
tag,
provider.name,
request_model,
providers.len(),
session_id
);
Ok(Self {
start_time,
app_config,
provider,
providers,
current_provider_id,
request_model,
tag,
app_type_str,
app_type,
session_id,
rectifier_config,
optimizer_config,
})
}
/// 从 URI 提取模型名称(Gemini 专用)
///
/// Gemini API 的模型名称在 URI 中,格式如:
/// `/v1beta/models/gemini-pro:generateContent`
pub fn with_model_from_uri(mut self, uri: &axum::http::Uri) -> Self {
let endpoint = uri
.path_and_query()
.map(|pq| pq.as_str())
.unwrap_or(uri.path());
self.request_model = endpoint
.split('/')
.find(|s| s.starts_with("models/"))
.and_then(|s| s.strip_prefix("models/"))
.map(|s| s.split(':').next().unwrap_or(s))
.unwrap_or("unknown")
.to_string();
self
}
/// 创建 RequestForwarder
///
/// 使用共享的 ProviderRouter,确保熔断器状态跨请求保持
///
/// 配置生效规则:
/// - 故障转移开启:超时配置正常生效(0 表示禁用超时)
/// - 故障转移关闭:超时配置不生效(全部传入 0)
pub fn create_forwarder(&self, state: &ProxyState) -> RequestForwarder {
let (non_streaming_timeout, first_byte_timeout, idle_timeout) =
if self.app_config.auto_failover_enabled {
// 故障转移开启:使用配置的值(0 = 禁用超时)
(
self.app_config.non_streaming_timeout as u64,
self.app_config.streaming_first_byte_timeout as u64,
self.app_config.streaming_idle_timeout as u64,
)
} else {
// 故障转移关闭:不启用超时配置
log::debug!(
"[{}] Failover disabled, timeout configs are bypassed",
self.tag
);
(0, 0, 0)
};
RequestForwarder::new(
state.provider_router.clone(),
non_streaming_timeout,
state.status.clone(),
state.current_providers.clone(),
state.failover_manager.clone(),
state.app_handle.clone(),
self.current_provider_id.clone(),
first_byte_timeout,
idle_timeout,
self.rectifier_config.clone(),
self.optimizer_config.clone(),
)
}
/// 获取 Provider 列表(用于故障转移)
///
/// 返回在创建上下文时已选择的 providers,避免重复调用 select_providers()
pub fn get_providers(&self) -> Vec<Provider> {
self.providers.clone()
}
/// 计算请求延迟(毫秒)
#[inline]
pub fn latency_ms(&self) -> u64 {
self.start_time.elapsed().as_millis() as u64
}
/// 获取流式超时配置
///
/// 配置生效规则:
/// - 故障转移开启:返回配置的值(0 表示禁用超时检查)
/// - 故障转移关闭:返回 0(禁用超时检查)
#[inline]
pub fn streaming_timeout_config(&self) -> StreamingTimeoutConfig {
if self.app_config.auto_failover_enabled {
// 故障转移开启:使用配置的值(0 = 禁用超时)
StreamingTimeoutConfig {
first_byte_timeout: self.app_config.streaming_first_byte_timeout as u64,
idle_timeout: self.app_config.streaming_idle_timeout as u64,
}
} else {
// 故障转移关闭:禁用流式超时检查
StreamingTimeoutConfig {
first_byte_timeout: 0,
idle_timeout: 0,
}
}
}
}