fix(usage): 流式终端 usage 以更完整值为准,Codex CLI 显式选择 response 解析器

- 新增 StandardizedUsage::signal_score/is_more_complete_than/choose_more_complete,流式合并与终端落库均按信号完整度择优
- OpenAI Chat/CLI 解析器支持仅 usage 的终结 chunk 与 response.completed usage
- Codex provider 注入 provider_stream_event_api_format=openai:cli,解析器选择改由 report_context 显式决定
- usage_mapper 扩展嵌套 response/message/item 兼容 Claude message_start/message_delta 及 Gemini stream chunks
- usage SQL upsert 在终态(completed/failed/cancelled)时按 GREATEST 写入 token/费用镜像列
This commit is contained in:
fawney19
2026-04-25 00:57:01 +08:00
parent 2c6209277f
commit f30b1f3f6b
14 changed files with 656 additions and 72 deletions

View File

@@ -83,6 +83,41 @@ impl StandardizedUsage {
}
self
}
pub fn signal_score(&self) -> usize {
[
self.input_tokens,
self.output_tokens,
self.cache_creation_tokens,
self.cache_creation_ephemeral_5m_tokens,
self.cache_creation_ephemeral_1h_tokens,
self.cache_read_tokens,
self.reasoning_tokens,
]
.into_iter()
.filter(|value| *value > 0)
.count()
+ self.dimensions.len()
}
pub fn has_token_signal(&self) -> bool {
self.signal_score() > 0
}
pub fn is_more_complete_than(&self, other: &Self) -> bool {
self.signal_score() > other.signal_score()
}
pub fn choose_more_complete(primary: Option<Self>, candidate: Option<Self>) -> Option<Self> {
match (primary, candidate) {
(Some(primary), Some(candidate)) if candidate.is_more_complete_than(&primary) => {
Some(candidate)
}
(Some(primary), _) => Some(primary),
(None, Some(candidate)) => Some(candidate),
(None, None) => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
@@ -116,6 +151,36 @@ fn as_f64(value: &serde_json::Value, default: f64) -> f64 {
mod tests {
use super::StandardizedUsage;
#[test]
fn standardized_usage_prefers_more_complete_candidate() {
let mut output_only = StandardizedUsage::new();
output_only.output_tokens = 131;
let mut complete = StandardizedUsage::new();
complete.input_tokens = 26;
complete.output_tokens = 131;
let selected = StandardizedUsage::choose_more_complete(Some(output_only), Some(complete))
.expect("usage should be selected");
assert_eq!(selected.input_tokens, 26);
assert_eq!(selected.output_tokens, 131);
}
#[test]
fn standardized_usage_keeps_primary_when_candidate_is_not_more_complete() {
let mut primary = StandardizedUsage::new();
primary.input_tokens = 26;
primary.output_tokens = 131;
let mut output_only = StandardizedUsage::new();
output_only.output_tokens = 131;
let selected = StandardizedUsage::choose_more_complete(Some(primary), Some(output_only))
.expect("usage should be selected");
assert_eq!(selected.input_tokens, 26);
assert_eq!(selected.output_tokens, 131);
}
#[test]
fn standardized_usage_reads_and_writes_known_and_extra_fields() {
let mut usage = StandardizedUsage::new();