优化: aichat效率批(save无变化捷径/断路器尾窗/system缓存指纹/会话列表刷新收敛) + 新增 LLM prompt caching(cache_control开关默认关/system稳定段易变段分离治缓存命中率) + 销账

This commit is contained in:
lxy
2026-08-09 21:35:13 +08:00
parent 4df91ed155
commit 7eb2e25ed5
13 changed files with 698 additions and 180 deletions
+86 -4
View File
@@ -29,10 +29,38 @@ use crate::retry::{
// 协议数据结构 + SSE 纯解析(apply_anthropic_event / AnthropicRequest 等)抽至 anthropic_helpers,
// 此处 use 引入以保持本模块内引用路径不变(零行为变更搬迁)。
use crate::anthropic_helpers::{
apply_anthropic_event, AnthropicRequest, AnthropicResponse, AnthropicToolDef,
apply_anthropic_event, AnthropicRequest, AnthropicResponse, AnthropicToolDef, SystemBlock,
ANTHROPIC_VERSION, DEFAULT_MAX_TOKENS,
};
// ============================================================
// Anthropic prompt caching 开关(进程级全局,受 AppState settings 同步)
// ============================================================
/// Anthropic prompt caching 总开关(默认关)。
///
/// 该分支同时服务 Claude 官方 / GLM 订阅端点 (open.bigmodel.cn/api/anthropic) / 任意 Messages
/// API 网关——**cache_control 字段可能被非官方网关拒收**,故必须默认关、用户对支持的端点显式开启。
///
/// 开关开:`convert_request` 把 system 包成 blocks 数组并附加 `cache_control:{type:ephemeral}`
/// (配合上层把易变段挪出 system,让 system 稳定段真正命中缓存)。
/// 开关关:system 保持纯字符串(现状行为,零风险兼容网关)。
///
/// 进程级全局而非 per-provider:上层(AppState settings)按「用户对当前启用的 anthropic 端点
/// 是否支持缓存」一次性开启,所有 anthropic 请求一致生效。默认关。
static ANTHROPIC_CACHE_ENABLED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
/// 设置 anthropic prompt caching 开关(上层读取 AppState settings 后同步)。
pub fn set_anthropic_cache_enabled(enabled: bool) {
ANTHROPIC_CACHE_ENABLED.store(enabled, std::sync::atomic::Ordering::SeqCst);
}
/// 读取 anthropic prompt caching 开关(`convert_request` 组装 system 形态用)。
pub fn anthropic_cache_enabled() -> bool {
ANTHROPIC_CACHE_ENABLED.load(std::sync::atomic::Ordering::SeqCst)
}
// ============================================================
// Provider 实现
// ============================================================
@@ -92,8 +120,10 @@ impl AnthropicCompatProvider {
req.model
};
// 抽离 system 消息
let system: Option<String> = {
// 抽离 system 消息。
// 开关开 → blocks 数组 + cache_control(Anthropic prompt caching,让 system 稳定段在
// 支持缓存的端点上命中);开关关 → 纯字符串(兼容非官方网关,零风险)。
let system: Option<SystemBlock> = {
let sys: Vec<String> = req
.messages
.iter()
@@ -102,8 +132,14 @@ impl AnthropicCompatProvider {
.collect();
if sys.is_empty() {
None
} else if anthropic_cache_enabled() {
Some(SystemBlock::Cached(vec![serde_json::json!({
"type": "text",
"text": sys.join("\n\n"),
"cache_control": { "type": "ephemeral" },
})]))
} else {
Some(sys.join("\n\n"))
Some(SystemBlock::Plain(sys.join("\n\n")))
}
};
@@ -1126,4 +1162,50 @@ mod tests {
"补占位后 precheck 应通过"
);
}
// ---------- prompt caching:开关驱动 system 形态 ----------
/// 开关控制 system 形态:开 → blocks 数组含 cache_control:ephemeral;关 → 纯字符串。
/// (两态共用进程级全局开关,拆两个用例并行跑会互相污染——单测内顺序切换断言,末尾复位。)
#[test]
fn anthropic_cache_switch_controls_system_shape() {
set_anthropic_cache_enabled(false); // 兜底复位,防污染其他并行用例
let provider = AnthropicCompatProvider::new("https://api.anthropic.com", "k", "claude-3-5-sonnet");
let mk = || CompletionRequest {
model: "claude-3-5-sonnet".into(),
messages: vec![
ChatMessage::system("a"),
ChatMessage::system("b"),
ChatMessage::user("hi"),
],
temperature: None,
max_tokens: None,
stream: false,
tools: None,
tool_choice: None,
reasoning_content: None,
};
// 开:数组形态 + cache_control(Anthropic prompt caching)
set_anthropic_cache_enabled(true);
let body = provider.convert_request(mk());
match body.system.expect("开关开时应有 system") {
SystemBlock::Cached(blocks) => {
assert_eq!(blocks.len(), 1, "开关开应包成单 blocks 数组");
assert_eq!(blocks[0]["type"], "text");
assert_eq!(blocks[0]["text"], "a\n\nb", "多条 system 应以 \\n\\n join 进单个 text 块");
assert_eq!(blocks[0]["cache_control"]["type"], "ephemeral");
}
SystemBlock::Plain(s) => panic!("开关开应为数组形态,实际 Plain: {}", s),
}
// 关:纯字符串(兼容非官方网关,零行为变更)
set_anthropic_cache_enabled(false);
let body = provider.convert_request(mk());
match body.system {
Some(SystemBlock::Plain(s)) => assert_eq!(s, "a\n\nb", "开关关多条 system 应以 \\n\\n join 纯字符串"),
other => panic!("开关关应为纯字符串,实际: {:?}", other),
}
}
}