优化: token分项显示(in/cache/out/reasoning)+ 详情面板 + base前置

token分项(各计费不同,不显 total):df-ai 解析 provider cache/reasoning(openai_compat prompt_cache_hit/miss/reasoning_tokens + anthropic cache_read/creation)+ TokenUsage 加字段(全构造点)+ AiMessage/AiCompleted/DB V39(ai_messages 加 cache_hit/miss/reasoning 列)+ message_repo 映射(持久化)+ 前端 MessageList 显 in·cache·out·reason(in=cache_miss 全价,reasoning 有才显)+ 点击 token 弹详情面板(完整 usage+缓存命中率+model)+ df-miniapp 同步

base前置(提升 prompt cache 命中率):chat.rs aug 拼 base 后(4处)+ knowledge_inject 知识拼 base 后(固定 base 前缀,cache 命中)

附修:replace_conversation 原 13 列 INSERT 丢消息级 token → 改 18 列
This commit is contained in:
lxy
2026-08-03 01:22:30 +08:00
parent 864c696b70
commit a031521776
25 changed files with 563 additions and 45 deletions
+78 -3
View File
@@ -1116,6 +1116,9 @@ pub(crate) async fn run_agentic_loop(
));
merge_msg.prompt_tokens = Some(0);
merge_msg.completion_tokens = Some(0);
merge_msg.prompt_cache_hit_tokens = Some(0);
merge_msg.prompt_cache_miss_tokens = Some(0);
merge_msg.reasoning_tokens = Some(0);
session.conv(&conv_id).messages.push(merge_msg);
if !merge_result.conflicts.is_empty() {
let mut conflict_msg = ChatMessage::assistant(&format!(
@@ -1128,6 +1131,9 @@ pub(crate) async fn run_agentic_loop(
));
conflict_msg.prompt_tokens = Some(0);
conflict_msg.completion_tokens = Some(0);
conflict_msg.prompt_cache_hit_tokens = Some(0);
conflict_msg.prompt_cache_miss_tokens = Some(0);
conflict_msg.reasoning_tokens = Some(0);
session.conv(&conv_id).messages.push(conflict_msg);
}
}
@@ -1139,6 +1145,9 @@ pub(crate) async fn run_agentic_loop(
prompt_tokens: tokens.prompt(),
completion_tokens: tokens.completion(),
total_tokens: tokens.total(),
prompt_cache_hit_tokens: tokens.cache_hit(),
prompt_cache_miss_tokens: tokens.cache_miss(),
reasoning_tokens: tokens.reasoning(),
};
finish_round_exit(
&session_arc, &db, &conv_id,
@@ -1176,6 +1185,9 @@ pub(crate) async fn run_agentic_loop(
prompt_tokens: tokens.prompt(),
completion_tokens: tokens.completion(),
total_tokens: tokens.total(),
prompt_cache_hit_tokens: tokens.cache_hit(),
prompt_cache_miss_tokens: tokens.cache_miss(),
reasoning_tokens: tokens.reasoning(),
};
// 入口 stop:本轮可能尚未 stream(首轮即停),不记 model——避免把未实际生成的 model 写入 models 数组
// 统一走 finish_round_exit:save(Some usage, None model) + spawn_title + emit(None,None,publish=true)
@@ -1548,8 +1560,13 @@ pub(crate) async fn run_agentic_loop(
prompt_tokens: if round_usage.prompt_tokens == 0 { estimated_prompt } else { round_usage.prompt_tokens },
completion_tokens: round_usage.completion_tokens,
total_tokens: if round_usage.prompt_tokens == 0 { estimated_prompt + round_usage.completion_tokens } else { round_usage.total_tokens },
// 分项 token(2026-08-02):cache/reasoning 透传自 round_usage,落库 + 累加器都需
prompt_cache_hit_tokens: round_usage.prompt_cache_hit_tokens,
prompt_cache_miss_tokens: round_usage.prompt_cache_miss_tokens,
reasoning_tokens: round_usage.reasoning_tokens,
};
tokens.add(usage.prompt_tokens, usage.completion_tokens);
// 累加本轮全量 usage(含 cache/reasoning 分项)到 tokens 累加器
tokens.add_usage(&usage);
// 追加 partial assistant 消息(若无 tool_calls 且有文本)
// 退出校验改 conv 存在性 + push 改 per_conv.messages。
@@ -1571,8 +1588,12 @@ pub(crate) async fn run_agentic_loop(
msg.reasoning_content = round_reasoning_content.clone();
// 消息级 token(对齐 push_assistant_message 双轨持久化):本轮 partial usage
// (prompt=round 或 estimated 兜底,completion=round)。系统提示消息无 token,不设。
// 分项 token(2026-08-02):cache/reasoning 透传自 round_usage。
msg.prompt_tokens = Some(usage.prompt_tokens);
msg.completion_tokens = Some(usage.completion_tokens);
msg.prompt_cache_hit_tokens = Some(usage.prompt_cache_hit_tokens);
msg.prompt_cache_miss_tokens = Some(usage.prompt_cache_miss_tokens);
msg.reasoning_tokens = Some(usage.reasoning_tokens);
conv.messages.push(msg);
// 追加系统提示消息:响应因网络中断不完整(对齐决策 a1 系统提示机制)
let mut notice = ChatMessage::system("⚠ 响应因网络中断不完整,以上为已接收的部分内容。可重新发送以获取完整回复。");
@@ -1590,6 +1611,9 @@ pub(crate) async fn run_agentic_loop(
prompt_tokens: tokens.prompt(),
completion_tokens: tokens.completion(),
total_tokens: usage.total_tokens,
prompt_cache_hit_tokens: tokens.cache_hit(),
prompt_cache_miss_tokens: tokens.cache_miss(),
reasoning_tokens: tokens.reasoning(),
};
finish_round_exit(
&session_arc, &db, &conv_id,
@@ -1608,9 +1632,23 @@ pub(crate) async fn run_agentic_loop(
// stream 后不再立即释放(会话级并发语义:工具执行期间也占槽)。per-provider permit
// (_provider_permit)仍在 candidate 循环内随作用域 Drop 自动释放。
// 累加本轮 token:provider 流式 usage 的 prompt_tokens 为 0 时(GLM 等),用预估输入兜底
// 累加本轮 token:provider 真实 usage。GLM 等流式不报 completion_tokens 为 0,如实反映(不预估)。
// 分项 token(2026-08-02):构造本轮完整 usage(prompt 用 estimated 兜底,cache/reasoning 透传 round),
// add_usage 一次性累加 prompt/completion/cache/reasoning 到 tokens 累加器。
let round_prompt = if round_usage.prompt_tokens == 0 { estimated_prompt } else { round_usage.prompt_tokens };
tokens.add(round_prompt, round_usage.completion_tokens);
let round_usage_full = df_ai::provider::TokenUsage {
prompt_tokens: round_prompt,
completion_tokens: round_usage.completion_tokens,
total_tokens: if round_usage.prompt_tokens == 0 {
estimated_prompt.saturating_add(round_usage.completion_tokens)
} else {
round_usage.total_tokens
},
prompt_cache_hit_tokens: round_usage.prompt_cache_hit_tokens,
prompt_cache_miss_tokens: round_usage.prompt_cache_miss_tokens,
reasoning_tokens: round_usage.reasoning_tokens,
};
tokens.add_usage(&round_usage_full);
// 追加 assistant 消息到历史 + G1 目标提取(扁平重构,原嵌套 8 层 → 3 层)
let has_tool_calls = !tool_calls_acc.is_empty();
@@ -1627,6 +1665,8 @@ pub(crate) async fn run_agentic_loop(
&mut session, &conv_id, has_tool_calls, &tool_calls_acc,
&full_text, &resolved_model, &last_reasoning_content,
round_prompt, round_usage.completion_tokens,
round_usage.prompt_cache_hit_tokens, round_usage.prompt_cache_miss_tokens,
round_usage.reasoning_tokens,
);
if GOAL_PIN_ENABLED {
update_pinned_goals(&mut session, &conv_id, &tool_calls_acc);
@@ -1642,6 +1682,9 @@ pub(crate) async fn run_agentic_loop(
prompt_tokens: tokens.prompt(),
completion_tokens: tokens.completion(),
total_tokens: tokens.total(),
prompt_cache_hit_tokens: tokens.cache_hit(),
prompt_cache_miss_tokens: tokens.cache_miss(),
reasoning_tokens: tokens.reasoning(),
};
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&resolved_model), true).await;
}
@@ -1652,6 +1695,9 @@ pub(crate) async fn run_agentic_loop(
prompt_tokens: tokens.prompt(),
completion_tokens: tokens.completion(),
total_tokens: tokens.total(),
prompt_cache_hit_tokens: tokens.cache_hit(),
prompt_cache_miss_tokens: tokens.cache_miss(),
reasoning_tokens: tokens.reasoning(),
};
// 统一走 finish_round_exit:save(Some usage, Some model) + spawn_title + emit(None,None,publish=true)
finish_round_exit(
@@ -1722,6 +1768,9 @@ pub(crate) async fn run_agentic_loop(
prompt_tokens: tokens.prompt(),
completion_tokens: tokens.completion(),
total_tokens: tokens.total(),
prompt_cache_hit_tokens: tokens.cache_hit(),
prompt_cache_miss_tokens: tokens.cache_miss(),
reasoning_tokens: tokens.reasoning(),
};
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&resolved_model), true).await;
// 审批等待 return 前 disarm guard——保持 generating=true 留 try_continue 续生成,
@@ -1750,6 +1799,9 @@ pub(crate) async fn run_agentic_loop(
prompt_tokens: tokens.prompt(),
completion_tokens: tokens.completion(),
total_tokens: tokens.total(),
prompt_cache_hit_tokens: tokens.cache_hit(),
prompt_cache_miss_tokens: tokens.cache_miss(),
reasoning_tokens: tokens.reasoning(),
};
// 统一走 finish_round_exit:save(Some usage, Some model) + 不 spawn_title(对齐原无 title) +
// emit(Some(true), publish=false)。**do_publish=false 保留原 max_iterations 不 publish 行为**
@@ -1773,6 +1825,9 @@ pub(crate) async fn run_agentic_loop(
prompt_tokens: tokens.prompt(),
completion_tokens: tokens.completion(),
total_tokens: tokens.total(),
prompt_cache_hit_tokens: tokens.cache_hit(),
prompt_cache_miss_tokens: tokens.cache_miss(),
reasoning_tokens: tokens.reasoning(),
};
// 落库 + 标题 + 知识提炼打包后台化:不阻塞 generating 复位与 Completed 事件
// save 先行(extract/title 都读已落库消息);extract 内部 fire-and-forget,与 title 可能并发
@@ -1806,6 +1861,9 @@ pub(crate) async fn run_agentic_loop(
prompt_tokens: tokens.prompt(),
completion_tokens: tokens.completion(),
total_tokens: usage_total,
prompt_cache_hit_tokens: tokens.cache_hit(),
prompt_cache_miss_tokens: tokens.cache_miss(),
reasoning_tokens: tokens.reasoning(),
};
emit_ai_completed_once(
&app_handle, &conv_id, &normal_usage,
@@ -2248,12 +2306,16 @@ async fn emit_ai_completed_once(
pinned_goals: &[super::GoalEntry],
) {
// emit 端(前端通道):incomplete 用 emit_incomplete(MidStream 传 Some(true))。
// token 分项(2026-08-02):cache_hit/cache_miss/reasoning 透传前端分计费展示。
let _ = app_handle.emit(
"ai-chat-event",
AiChatEvent::AiCompleted {
total_tokens: usage.total_tokens,
prompt_tokens: usage.prompt_tokens,
completion_tokens: usage.completion_tokens,
prompt_cache_hit_tokens: usage.prompt_cache_hit_tokens,
prompt_cache_miss_tokens: usage.prompt_cache_miss_tokens,
reasoning_tokens: usage.reasoning_tokens,
incomplete: emit_incomplete,
conversation_id: Some(conv_id.to_string()),
pinned_goals: pinned_goals.to_vec(),
@@ -2267,6 +2329,9 @@ async fn emit_ai_completed_once(
total_tokens: usage.total_tokens,
prompt_tokens: usage.prompt_tokens,
completion_tokens: usage.completion_tokens,
prompt_cache_hit_tokens: usage.prompt_cache_hit_tokens,
prompt_cache_miss_tokens: usage.prompt_cache_miss_tokens,
reasoning_tokens: usage.reasoning_tokens,
incomplete: publish_incomplete,
conversation_id: Some(conv_id.to_string()),
pinned_goals: pinned_goals.to_vec(),
@@ -2335,6 +2400,7 @@ async fn finish_round_exit(
// 入参 session 需外层调用方持锁;本函数只做纯内存 mutate,无 await/emit,不会死锁。
// prompt_tokens/completion_tokens: 本轮 LLM 调用 token 用量(消息级持久化,解 reload/压缩/切会话后
// 历史 assistant 消息 token 不显)。两构造分支都设。
// 分项 token(2026-08-02):cache_hit/cache_miss/reasoning 透传自 round_usage,前端分计费展示。
fn push_assistant_message(
session: &mut AiSession,
conv_id: &str,
@@ -2345,6 +2411,9 @@ fn push_assistant_message(
last_reasoning_content: &Option<String>,
prompt_tokens: u32,
completion_tokens: u32,
cache_hit: u32,
cache_miss: u32,
reasoning: u32,
) {
if has_tool_calls {
let mut order: Vec<u32> = tool_calls_acc.keys().copied().collect();
@@ -2368,6 +2437,9 @@ fn push_assistant_message(
msg.reasoning_content = last_reasoning_content.clone();
msg.prompt_tokens = Some(prompt_tokens);
msg.completion_tokens = Some(completion_tokens);
msg.prompt_cache_hit_tokens = Some(cache_hit);
msg.prompt_cache_miss_tokens = Some(cache_miss);
msg.reasoning_tokens = Some(reasoning);
session.conv(conv_id).messages.push(msg);
} else if !full_text.is_empty() {
let mut msg = ChatMessage::assistant(full_text);
@@ -2375,6 +2447,9 @@ fn push_assistant_message(
msg.reasoning_content = last_reasoning_content.clone();
msg.prompt_tokens = Some(prompt_tokens);
msg.completion_tokens = Some(completion_tokens);
msg.prompt_cache_hit_tokens = Some(cache_hit);
msg.prompt_cache_miss_tokens = Some(cache_miss);
msg.reasoning_tokens = Some(reasoning);
session.conv(conv_id).messages.push(msg);
}
}