优化: 上下文管理与提示词链路批
- T1 usage 口径: anthropic usage 两层容错+cache 三口径归一(prompt=input+read+creation) - T2 压缩水位判据: compress_should_trigger 双模式+ratio 钳位,开关关逐字节等价 - T3 跨会话检索: history_search 工具(两跳式)+message_repo 关键词检索+prompt 指引 - T4 代语义闭环: status 终局补写+飞书停靠/换代推卡+交付引导+watcher 兜底补漏 - 段序缓存重排: build_runtime_prompt 恒静段上移/工作上下文压尾(前缀缓存失效面收窄) - 智谱缓存解析: anthropic 臂补 prompt_tokens_details.cached_tokens 嵌套回退 - 工时禁令: 提示词双语加「禁止预估工时,AI Coding 工时无意义」 - 拖拽文件落盘: 非图片附件存引用行,防整段文本灌入输入框 - 事实修正: 提示词「40+ 工具」→「80+」(基线 81);tool_failure_memory 测试串行锁除竞态 - UI 拆分: TopBar/ToolResultBody 壳层化(top-bar/tool-result-body 子件目录)
This commit is contained in:
@@ -806,12 +806,21 @@ impl LlmProvider for AnthropicCompatProvider {
|
||||
other => warn!(block_type = other, "Anthropic 响应含未知 content 块类型,已忽略"),
|
||||
}
|
||||
}
|
||||
// 口径归一(G1,P0-A/u-claw 三口径,与 anthropic_helpers message_start 同表):
|
||||
// anthropic input_tokens 只含未缓存输入,不含 cache_read/creation 两段;
|
||||
// 落库 prompt_tokens 对齐 openai/deepseek 全量输入语义(hit/miss ⊆ prompt),
|
||||
// 故 prompt = input + read + creation,total = prompt + output。
|
||||
// cache 字段:creation(写入)= miss 全价语义,read(命中)= hit 低价。
|
||||
let prompt_tokens = resp
|
||||
.usage
|
||||
.input_tokens
|
||||
.saturating_add(resp.usage.cache_read())
|
||||
.saturating_add(resp.usage.cache_creation_input_tokens);
|
||||
let usage = TokenUsage {
|
||||
prompt_tokens: resp.usage.input_tokens,
|
||||
prompt_tokens,
|
||||
completion_tokens: resp.usage.output_tokens,
|
||||
total_tokens: resp.usage.input_tokens + resp.usage.output_tokens,
|
||||
// anthropic cache 字段:creation(写入)= miss 全价语义,read(命中)= hit 低价
|
||||
prompt_cache_hit_tokens: resp.usage.cache_read_input_tokens,
|
||||
total_tokens: prompt_tokens.saturating_add(resp.usage.output_tokens),
|
||||
prompt_cache_hit_tokens: resp.usage.cache_read(),
|
||||
prompt_cache_miss_tokens: resp.usage.cache_creation_input_tokens,
|
||||
reasoning_tokens: 0,
|
||||
};
|
||||
@@ -1016,6 +1025,47 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
/// 辅助:构造带缓存分项的 message_start 事件(cache_read/cache_creation,P0-A 三口径)
|
||||
fn message_start_with_cache(input_tokens: u32, cache_read: u32, cache_creation: u32) -> String {
|
||||
format!(
|
||||
r#"{{"type":"message_start","message":{{"usage":{{"input_tokens":{},"cache_read_input_tokens":{},"cache_creation_input_tokens":{},"output_tokens":0}}}}}}"#,
|
||||
input_tokens, cache_read, cache_creation
|
||||
)
|
||||
}
|
||||
|
||||
/// 辅助:构造智谱 anthropic 兼容端点形态的 message_start(隐式缓存命中走嵌套
|
||||
/// prompt_tokens_details.cached_tokens,无顶层 cache 字段——官方缓存文档口径)
|
||||
fn message_start_zhipu_nested(input_tokens: u32, cached_tokens: u32) -> String {
|
||||
format!(
|
||||
r#"{{"type":"message_start","message":{{"usage":{{"input_tokens":{},"prompt_tokens_details":{{"cached_tokens":{}}},"output_tokens":0}}}}}}"#,
|
||||
input_tokens, cached_tokens
|
||||
)
|
||||
}
|
||||
|
||||
/// 智谱嵌套形态回退:顶层 cache 字段缺席时从 prompt_tokens_details.cached_tokens
|
||||
/// 取命中量,prompt=input+cached(隐式缓存无写入分项,miss=0)
|
||||
#[test]
|
||||
fn anthropic_zhipu_nested_cache_tokens_fallback() {
|
||||
let mut acc: Option<TokenUsage> = None;
|
||||
let c = apply_anthropic_event(&message_start_zhipu_nested(500, 4000), &mut acc);
|
||||
let u = c.usage.expect("usage 应随 message_start 带出");
|
||||
assert_eq!(u.prompt_cache_hit_tokens, 4000);
|
||||
assert_eq!(u.prompt_cache_miss_tokens, 0);
|
||||
// 口径归一:prompt = input + cached
|
||||
assert_eq!(u.prompt_tokens, 4500);
|
||||
}
|
||||
|
||||
/// 顶层优先:官方 Anthropic 端点回顶层字段时嵌套值(即便异常并存)不覆盖顶层
|
||||
#[test]
|
||||
fn anthropic_top_level_cache_read_wins_over_nested() {
|
||||
let mut acc: Option<TokenUsage> = None;
|
||||
let frame = r#"{"type":"message_start","message":{"usage":{"input_tokens":100,"cache_read_input_tokens":200,"prompt_tokens_details":{"cached_tokens":999},"output_tokens":0}}}"#;
|
||||
let c = apply_anthropic_event(frame, &mut acc);
|
||||
let u = c.usage.expect("usage 应随 message_start 带出");
|
||||
assert_eq!(u.prompt_cache_hit_tokens, 200);
|
||||
assert_eq!(u.prompt_tokens, 300);
|
||||
}
|
||||
|
||||
/// 辅助:构造 message_delta 事件(output_tokens 为累计值)
|
||||
fn message_delta(output_tokens: u32) -> String {
|
||||
format!(
|
||||
@@ -1135,6 +1185,48 @@ mod tests {
|
||||
assert!(c.usage.is_none(), "无 usage 时 message_stop usage 为 None");
|
||||
}
|
||||
|
||||
/// 三口径映射锁定(G1,P0-A):anthropic input_tokens 不含缓存两段,落库 prompt_tokens
|
||||
/// 对齐 openai/deepseek 全量输入语义(hit/miss ⊆ prompt)→ prompt = input + read +
|
||||
/// creation;message_delta 的 total 重算基于累加器 prompt(已含缓存段)自动继承新口径。
|
||||
#[test]
|
||||
fn anthropic_message_start_cache_three_caliber_prompt_sums_cache_segments() {
|
||||
let mut acc: Option<TokenUsage> = None;
|
||||
let c = apply_anthropic_event(&message_start_with_cache(100, 64, 36), &mut acc);
|
||||
let u0 = c.usage.expect("message_start 应带出初始 usage 快照");
|
||||
assert_eq!(u0.prompt_tokens, 200, "prompt = input(100)+read(64)+creation(36)");
|
||||
assert_eq!(u0.prompt_cache_hit_tokens, 64, "cache_read → hit 原样映射");
|
||||
assert_eq!(u0.prompt_cache_miss_tokens, 36, "cache_creation → miss 原样映射");
|
||||
assert_eq!(u0.total_tokens, 200, "初始 total 同口径");
|
||||
|
||||
// message_delta 累计 output 后 total 重算:继承含缓存段 prompt 基数(200+30)
|
||||
let _ = apply_anthropic_event(&message_delta(30), &mut acc);
|
||||
let a = acc.as_ref().unwrap();
|
||||
assert_eq!(a.prompt_tokens, 200, "delta 不改 prompt");
|
||||
assert_eq!(a.total_tokens, 230, "total = prompt(含缓存两段)+output");
|
||||
}
|
||||
|
||||
/// 缺失容错(G2,P0-A):端点 200 但 usage 键整体缺失/usage 对象缺计数字段时,
|
||||
/// 不再整帧反序列化失败硬炸(三路走查 #3「有效文本回复整体丢弃」),全零兜底
|
||||
/// 下行走既有估算轨(round_usage_effective prompt==0 → 估算打标),对齐 OpenAI 臂。
|
||||
#[test]
|
||||
fn anthropic_response_missing_or_partial_usage_deserializes_not_fatal() {
|
||||
// usage 键整体缺失
|
||||
let resp: AnthropicResponse = serde_json::from_str(
|
||||
r#"{"id":"msg_1","model":"claude-x","content":[{"type":"text","text":"hi"}],"stop_reason":"end_turn"}"#,
|
||||
)
|
||||
.expect("usage 键缺失不应炸帧");
|
||||
assert_eq!(resp.usage.input_tokens, 0, "缺 usage → 全零兜底走估算轨");
|
||||
assert_eq!(resp.usage.output_tokens, 0);
|
||||
|
||||
// usage 对象存在但缺计数字段(部分网关只回 cache 分项)
|
||||
let resp2: AnthropicResponse = serde_json::from_str(
|
||||
r#"{"id":"msg_2","model":"claude-x","content":[],"usage":{"cache_read_input_tokens":7}}"#,
|
||||
)
|
||||
.expect("usage 缺 input/output 字段不应炸帧");
|
||||
assert_eq!(resp2.usage.input_tokens, 0);
|
||||
assert_eq!(resp2.usage.cache_read_input_tokens, 7, "已有分项不丢");
|
||||
}
|
||||
|
||||
/// content_block_start (tool_use) 带 id+name
|
||||
#[test]
|
||||
fn anthropic_content_block_start_tool_use() {
|
||||
|
||||
@@ -69,6 +69,10 @@ pub(crate) struct AnthropicResponse {
|
||||
// Anthropic 响应反序列化字段,保留以备调试/未来消费(如日志记录调用终止原因),标注意图消除 dead_code warning
|
||||
#[allow(dead_code)]
|
||||
stop_reason: Option<String>,
|
||||
/// usage 整段 default(G2):端点不回 usage 键时不再 missing-field 炸帧,全零兜底
|
||||
/// 下行走既有估算轨(round_usage_effective)。字段级容错在 AnthropicUsage 侧,
|
||||
/// 此处兜「usage 键整体缺失」——两层合起来才覆盖「端点不回 usage」全形态。
|
||||
#[serde(default)]
|
||||
pub usage: AnthropicUsage,
|
||||
}
|
||||
|
||||
@@ -88,9 +92,15 @@ pub(crate) struct AnthropicContentBlock {
|
||||
pub input: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub(crate) struct AnthropicUsage {
|
||||
/// 计数字段全部 default 容错(G2,P0-A 口径统一):部分网关 200 但 usage 缺失/缺字段,
|
||||
/// 必填会让整帧反序列化失败 → 有效回复整体丢弃(三路走查 #3/#5)。缺省 0 后全零
|
||||
/// TokenUsage 下行走既有估算兜底轨(round_usage_effective,prompt==0 → 估算打标),
|
||||
/// 与 OpenAI 臂的 serde default 容错(openai_helpers.rs:139-157)对齐。
|
||||
#[serde(default)]
|
||||
pub input_tokens: u32,
|
||||
#[serde(default)]
|
||||
pub output_tokens: u32,
|
||||
/// Anthropic prompt caching 扩展:cache 写入 token(本次写入缓存,计费如输入但稍便宜)。
|
||||
/// 映射到 TokenUsage.prompt_cache_miss_tokens(全价输入语义)。
|
||||
@@ -102,6 +112,32 @@ pub(crate) struct AnthropicUsage {
|
||||
/// 非 cache 场景无此字段 → serde default 0。
|
||||
#[serde(default)]
|
||||
pub cache_read_input_tokens: u32,
|
||||
/// 智谱 anthropic 兼容端点的隐式缓存(自动生效,命中半价)不回顶层 cache 字段,
|
||||
/// 命中量走 OpenAI 风格嵌套 prompt_tokens_details.cached_tokens(智谱官方缓存文档
|
||||
/// 口径)。serde 容错读入,消费侧经 [`Self::cache_read`] 回退取用——缺此层时
|
||||
/// GLM 链路账本/命中率恒 0(实测 54 条观测全零的根因)。
|
||||
#[serde(default)]
|
||||
pub prompt_tokens_details: Option<PromptTokensDetails>,
|
||||
}
|
||||
|
||||
/// OpenAI 风格嵌套的缓存命中分项(智谱 anthropic 兼容端点口径)。
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub(crate) struct PromptTokensDetails {
|
||||
#[serde(default)]
|
||||
pub cached_tokens: u32,
|
||||
}
|
||||
|
||||
impl AnthropicUsage {
|
||||
/// 缓存命中读取量:顶层 cache_read_input_tokens 非零优先(官方 Anthropic 端点),
|
||||
/// 智谱嵌套形态(顶层缺省 0)回退;两处都不存在时 0。
|
||||
pub fn cache_read(&self) -> u32 {
|
||||
if self.cache_read_input_tokens > 0 {
|
||||
return self.cache_read_input_tokens;
|
||||
}
|
||||
self.prompt_tokens_details
|
||||
.as_ref()
|
||||
.map_or(0, |d| d.cached_tokens)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -155,15 +191,30 @@ pub(crate) fn apply_anthropic_event(data: &str, usage_accum: &mut Option<TokenUs
|
||||
let cache_read = u_obj
|
||||
.and_then(|u| u.get("cache_read_input_tokens"))
|
||||
.and_then(|t| t.as_u64())
|
||||
// 智谱 anthropic 兼容端点隐式缓存走嵌套 prompt_tokens_details.cached_tokens,
|
||||
// 顶层字段缺席时回退读取(与非流式 AnthropicUsage::cache_read 同表)。
|
||||
.or_else(|| {
|
||||
u_obj
|
||||
.and_then(|u| u.get("prompt_tokens_details"))
|
||||
.and_then(|d| d.get("cached_tokens"))
|
||||
.and_then(|t| t.as_u64())
|
||||
})
|
||||
.unwrap_or(0) as u32;
|
||||
let cache_creation = u_obj
|
||||
.and_then(|u| u.get("cache_creation_input_tokens"))
|
||||
.and_then(|t| t.as_u64())
|
||||
.unwrap_or(0) as u32;
|
||||
// 口径归一(G1,P0-A/u-claw 三口径): anthropic input_tokens 只含未缓存输入,
|
||||
// 不含 cache_read/cache_creation 两段;落库 prompt_tokens 对齐 openai/deepseek
|
||||
// 全量输入语义(prompt=全量输入,hit/miss 为其子集),故 prompt = input + read
|
||||
// + creation。total 同口径(prompt + 当前 output)。
|
||||
let prompt = (inp as u32)
|
||||
.saturating_add(cache_read)
|
||||
.saturating_add(cache_creation);
|
||||
*usage_accum = Some(TokenUsage {
|
||||
prompt_tokens: inp as u32,
|
||||
prompt_tokens: prompt,
|
||||
completion_tokens: 0,
|
||||
total_tokens: inp as u32,
|
||||
total_tokens: prompt,
|
||||
prompt_cache_hit_tokens: cache_read,
|
||||
prompt_cache_miss_tokens: cache_creation,
|
||||
reasoning_tokens: 0,
|
||||
|
||||
Reference in New Issue
Block a user