优化: aichat效率批(save无变化捷径/断路器尾窗/system缓存指纹/会话列表刷新收敛) + 新增 LLM prompt caching(cache_control开关默认关/system稳定段易变段分离治缓存命中率) + 销账
This commit is contained in:
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,20 @@ use crate::provider::{tool_call_id_or_fallback, StreamChunk, TokenUsage, ToolCal
|
||||
// Anthropic API 请求/响应结构体
|
||||
// ============================================================
|
||||
|
||||
/// Anthropic `system` 字段形态(受 prompt caching 开关 `ANTHROPIC_CACHE_ENABLED` 控制):
|
||||
/// - `Plain`:纯字符串。开关关时保持原形态,兼容非官方网关(GLM 订阅端点 / 任意 Messages API
|
||||
/// 代理——部分网关会拒收 cache_control 字段)。
|
||||
/// - `Cached`:blocks 数组 `[{type:text, text, cache_control:{type:ephemeral}}]`。开关开时启用
|
||||
/// Anthropic prompt caching,让 system 稳定段在支持缓存的端点上命中。
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub(crate) enum SystemBlock {
|
||||
/// 纯字符串形态(兼容网关,开关关)
|
||||
Plain(String),
|
||||
/// blocks 数组形态(含 cache_control,开关开)
|
||||
Cached(Vec<serde_json::Value>),
|
||||
}
|
||||
|
||||
/// Anthropic 请求体
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub(crate) struct AnthropicRequest {
|
||||
@@ -25,7 +39,7 @@ pub(crate) struct AnthropicRequest {
|
||||
pub messages: Vec<serde_json::Value>,
|
||||
pub max_tokens: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub system: Option<String>,
|
||||
pub system: Option<SystemBlock>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub temperature: Option<f32>,
|
||||
pub stream: bool,
|
||||
|
||||
@@ -275,6 +275,17 @@ impl ContextManager {
|
||||
self.messages.iter().map(|t| t.message.clone()).collect()
|
||||
}
|
||||
|
||||
/// 只读最近 N 条消息(尾部切片 clone)。BE2(AC-EFF-R2-1):断路器/探索熔断检查等只关心
|
||||
/// 最近消息的读路径,用本方法避免 all_messages_clone 每轮全量 clone(长对话每轮 O(n) 深克隆浪费)。
|
||||
/// 尾部顺序保持时间正序(与 all_messages_clone 一致,调用方从尾部反向扫即可)。
|
||||
pub fn recent_messages(&self, n: usize) -> Vec<ChatMessage> {
|
||||
let skip = self.messages.len().saturating_sub(n);
|
||||
self.messages[skip..]
|
||||
.iter()
|
||||
.map(|t| t.message.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 从 Vec 恢复(兼容从 DB 加载)
|
||||
pub fn restore_from_messages(&mut self, messages: Vec<ChatMessage>) {
|
||||
// clear() 会置 needs_full_rewrite=true + persisted_msg_count=0;但本入口是"DB 刚加载",
|
||||
|
||||
Reference in New Issue
Block a user