feat(provider): 缓存命中诊断日志与两种 usage 缓存格式解析测试
排查平台有缓存命中但前端占比为 0 的问题:set_usage/非流式/回退三处输出 cache diagnostics 日志;新增 DeepSeek 顶层与 OpenAI 嵌套缓存字段解析回归测试
This commit is contained in:
parent
b26a2c2512
commit
011b23b54e
@ -96,6 +96,7 @@ impl StreamingAccumulator {
|
|||||||
/// 跳过 total_tokens=0 的占位帧,避免覆盖真实值。
|
/// 跳过 total_tokens=0 的占位帧,避免覆盖真实值。
|
||||||
fn set_usage(&mut self, usage: OpenAIUsage) {
|
fn set_usage(&mut self, usage: OpenAIUsage) {
|
||||||
if usage.total_tokens > 0 {
|
if usage.total_tokens > 0 {
|
||||||
|
usage.log_cache_diagnostics("stream_final_frame");
|
||||||
self.usage = Some(usage);
|
self.usage = Some(usage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -724,6 +725,7 @@ impl OpenAIProvider {
|
|||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
// 回退场景下也从非流式响应提取 usage
|
// 回退场景下也从非流式响应提取 usage
|
||||||
|
openai_resp.usage.log_cache_diagnostics("non_streaming_fallback");
|
||||||
response.usage = Usage {
|
response.usage = Usage {
|
||||||
prompt_tokens: openai_resp.usage.prompt_tokens,
|
prompt_tokens: openai_resp.usage.prompt_tokens,
|
||||||
completion_tokens: openai_resp.usage.completion_tokens,
|
completion_tokens: openai_resp.usage.completion_tokens,
|
||||||
@ -1106,6 +1108,19 @@ impl OpenAIUsage {
|
|||||||
})
|
})
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 诊断日志:记录 API 是否返回缓存字段及解析后的值(排查"缓存一直不命中"问题)。
|
||||||
|
/// field_present=false 说明 API/网关根本没返回缓存字段(中转服务剥离或模型不支持)。
|
||||||
|
fn log_cache_diagnostics(&self, source: &str) {
|
||||||
|
tracing::info!(
|
||||||
|
source = %source,
|
||||||
|
prompt_tokens = self.prompt_tokens,
|
||||||
|
cached_tokens = self.cached_tokens(),
|
||||||
|
deepseek_cache_field_present = self.prompt_cache_hit_tokens.is_some(),
|
||||||
|
openai_cache_details_present = self.prompt_tokens_details.is_some(),
|
||||||
|
"OpenAI usage cache diagnostics"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@ -1263,11 +1278,14 @@ impl LLMProvider for OpenAIProvider {
|
|||||||
content,
|
content,
|
||||||
reasoning_content: openai_resp.choices[0].message.reasoning_content.clone(),
|
reasoning_content: openai_resp.choices[0].message.reasoning_content.clone(),
|
||||||
tool_calls,
|
tool_calls,
|
||||||
usage: Usage {
|
usage: {
|
||||||
prompt_tokens: openai_resp.usage.prompt_tokens,
|
openai_resp.usage.log_cache_diagnostics("non_streaming");
|
||||||
completion_tokens: openai_resp.usage.completion_tokens,
|
Usage {
|
||||||
total_tokens: openai_resp.usage.total_tokens,
|
prompt_tokens: openai_resp.usage.prompt_tokens,
|
||||||
cached_tokens: openai_resp.usage.cached_tokens(),
|
completion_tokens: openai_resp.usage.completion_tokens,
|
||||||
|
total_tokens: openai_resp.usage.total_tokens,
|
||||||
|
cached_tokens: openai_resp.usage.cached_tokens(),
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -1571,6 +1589,45 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_usage_parses_deepseek_cache_fields() {
|
||||||
|
// DeepSeek 官方 API 的 usage 顶层缓存字段
|
||||||
|
let usage: OpenAIUsage = serde_json::from_value(json!({
|
||||||
|
"prompt_tokens": 345,
|
||||||
|
"completion_tokens": 564,
|
||||||
|
"total_tokens": 909,
|
||||||
|
"prompt_cache_hit_tokens": 128,
|
||||||
|
"prompt_cache_miss_tokens": 217
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(usage.cached_tokens(), 128);
|
||||||
|
assert!(usage.prompt_cache_hit_tokens.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_usage_parses_openai_nested_cache_details() {
|
||||||
|
// OpenAI 兼容格式:嵌套在 prompt_tokens_details 中
|
||||||
|
let usage: OpenAIUsage = serde_json::from_value(json!({
|
||||||
|
"prompt_tokens": 1000,
|
||||||
|
"completion_tokens": 100,
|
||||||
|
"total_tokens": 1100,
|
||||||
|
"prompt_tokens_details": { "cached_tokens": 512 }
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(usage.cached_tokens(), 512);
|
||||||
|
|
||||||
|
// 无缓存字段时回退为 0
|
||||||
|
let no_cache: OpenAIUsage = serde_json::from_value(json!({
|
||||||
|
"prompt_tokens": 10,
|
||||||
|
"completion_tokens": 5,
|
||||||
|
"total_tokens": 15
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(no_cache.cached_tokens(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_openai_response_parses_json_tool_arguments() {
|
fn test_openai_response_parses_json_tool_arguments() {
|
||||||
let response: OpenAIResponse = serde_json::from_value(json!({
|
let response: OpenAIResponse = serde_json::from_value(json!({
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user