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

@@ -6323,18 +6323,28 @@ ORDER BY "usage".user_id ASC
.bind(&usage.provider_endpoint_kind)
.bind(usage.has_format_conversion)
.bind(usage.is_stream)
.bind(None::<i32>)
.bind(None::<i32>)
.bind(None::<i32>)
.bind(None::<i32>)
.bind(None::<i32>)
.bind(None::<i32>)
.bind(None::<i32>)
.bind(None::<f64>)
.bind(None::<f64>)
.bind(None::<f64>)
.bind(None::<f64>)
.bind(usage.input_tokens.map(to_i32).transpose()?)
.bind(usage.output_tokens.map(to_i32).transpose()?)
.bind(usage.total_tokens.map(to_i32).transpose()?)
.bind(usage.cache_creation_input_tokens.map(to_i32).transpose()?)
.bind(
usage
.cache_creation_ephemeral_5m_input_tokens
.map(to_i32)
.transpose()?,
)
.bind(
usage
.cache_creation_ephemeral_1h_input_tokens
.map(to_i32)
.transpose()?,
)
.bind(usage.cache_read_input_tokens.map(to_i32).transpose()?)
.bind(usage.cache_creation_cost_usd)
.bind(usage.cache_read_cost_usd)
.bind(None::<f64>)
.bind(usage.total_cost_usd)
.bind(usage.actual_total_cost_usd)
.bind(usage.status_code.map(i32::from))
.bind(&usage.error_message)
.bind(&usage.error_category)

View File

@@ -135,18 +135,18 @@ DO UPDATE SET
provider_endpoint_kind = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.provider_endpoint_kind, "usage".provider_endpoint_kind) ELSE "usage".provider_endpoint_kind END,
has_format_conversion = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.has_format_conversion, "usage".has_format_conversion) ELSE "usage".has_format_conversion END,
is_stream = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.is_stream, "usage".is_stream) ELSE "usage".is_stream END,
input_tokens = "usage".input_tokens,
output_tokens = "usage".output_tokens,
total_tokens = "usage".total_tokens,
cache_creation_input_tokens = "usage".cache_creation_input_tokens,
cache_creation_input_tokens_5m = "usage".cache_creation_input_tokens_5m,
cache_creation_input_tokens_1h = "usage".cache_creation_input_tokens_1h,
cache_read_input_tokens = "usage".cache_read_input_tokens,
cache_creation_cost_usd = "usage".cache_creation_cost_usd,
cache_read_cost_usd = "usage".cache_read_cost_usd,
input_tokens = CASE WHEN "usage".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST("usage".input_tokens, EXCLUDED.input_tokens) ELSE "usage".input_tokens END,
output_tokens = CASE WHEN "usage".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST("usage".output_tokens, EXCLUDED.output_tokens) ELSE "usage".output_tokens END,
total_tokens = CASE WHEN "usage".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST("usage".total_tokens, EXCLUDED.total_tokens) ELSE "usage".total_tokens END,
cache_creation_input_tokens = CASE WHEN "usage".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST("usage".cache_creation_input_tokens, EXCLUDED.cache_creation_input_tokens) ELSE "usage".cache_creation_input_tokens END,
cache_creation_input_tokens_5m = CASE WHEN "usage".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST("usage".cache_creation_input_tokens_5m, EXCLUDED.cache_creation_input_tokens_5m) ELSE "usage".cache_creation_input_tokens_5m END,
cache_creation_input_tokens_1h = CASE WHEN "usage".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST("usage".cache_creation_input_tokens_1h, EXCLUDED.cache_creation_input_tokens_1h) ELSE "usage".cache_creation_input_tokens_1h END,
cache_read_input_tokens = CASE WHEN "usage".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST("usage".cache_read_input_tokens, EXCLUDED.cache_read_input_tokens) ELSE "usage".cache_read_input_tokens END,
cache_creation_cost_usd = CASE WHEN "usage".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST("usage".cache_creation_cost_usd, EXCLUDED.cache_creation_cost_usd) ELSE "usage".cache_creation_cost_usd END,
cache_read_cost_usd = CASE WHEN "usage".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST("usage".cache_read_cost_usd, EXCLUDED.cache_read_cost_usd) ELSE "usage".cache_read_cost_usd END,
output_price_per_1m = NULL,
total_cost_usd = "usage".total_cost_usd,
actual_total_cost_usd = "usage".actual_total_cost_usd,
total_cost_usd = CASE WHEN "usage".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST("usage".total_cost_usd, EXCLUDED.total_cost_usd) ELSE "usage".total_cost_usd END,
actual_total_cost_usd = CASE WHEN "usage".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST("usage".actual_total_cost_usd, EXCLUDED.actual_total_cost_usd) ELSE "usage".actual_total_cost_usd END,
status_code = CASE WHEN "usage".billing_status = 'pending' THEN CASE
WHEN "usage".status = 'streaming' AND EXCLUDED.status = 'pending' THEN "usage".status_code
WHEN EXCLUDED.status IN ('pending', 'streaming', 'completed', 'cancelled') AND EXCLUDED.status_code IS NULL THEN NULL

View File

@@ -585,27 +585,46 @@ fn usage_sql_writes_usage_settlement_pricing_snapshots() {
}
#[test]
fn usage_sql_does_not_update_deprecated_billing_mirror_columns() {
fn usage_sql_updates_usage_mirror_columns_from_terminal_events_only() {
for assignment in [
"input_tokens = \"usage\".input_tokens",
"output_tokens = \"usage\".output_tokens",
"total_tokens = \"usage\".total_tokens",
"cache_creation_input_tokens = \"usage\".cache_creation_input_tokens",
"cache_creation_input_tokens_5m = \"usage\".cache_creation_input_tokens_5m",
"cache_creation_input_tokens_1h = \"usage\".cache_creation_input_tokens_1h",
"cache_read_input_tokens = \"usage\".cache_read_input_tokens",
"cache_creation_cost_usd = \"usage\".cache_creation_cost_usd",
"cache_read_cost_usd = \"usage\".cache_read_cost_usd",
"total_cost_usd = \"usage\".total_cost_usd",
"actual_total_cost_usd = \"usage\".actual_total_cost_usd",
"input_tokens = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".input_tokens, EXCLUDED.input_tokens) ELSE \"usage\".input_tokens END",
"output_tokens = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".output_tokens, EXCLUDED.output_tokens) ELSE \"usage\".output_tokens END",
"total_tokens = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".total_tokens, EXCLUDED.total_tokens) ELSE \"usage\".total_tokens END",
"cache_creation_input_tokens = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".cache_creation_input_tokens, EXCLUDED.cache_creation_input_tokens) ELSE \"usage\".cache_creation_input_tokens END",
"cache_creation_input_tokens_5m = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".cache_creation_input_tokens_5m, EXCLUDED.cache_creation_input_tokens_5m) ELSE \"usage\".cache_creation_input_tokens_5m END",
"cache_creation_input_tokens_1h = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".cache_creation_input_tokens_1h, EXCLUDED.cache_creation_input_tokens_1h) ELSE \"usage\".cache_creation_input_tokens_1h END",
"cache_read_input_tokens = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".cache_read_input_tokens, EXCLUDED.cache_read_input_tokens) ELSE \"usage\".cache_read_input_tokens END",
"cache_creation_cost_usd = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".cache_creation_cost_usd, EXCLUDED.cache_creation_cost_usd) ELSE \"usage\".cache_creation_cost_usd END",
"cache_read_cost_usd = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".cache_read_cost_usd, EXCLUDED.cache_read_cost_usd) ELSE \"usage\".cache_read_cost_usd END",
"total_cost_usd = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".total_cost_usd, EXCLUDED.total_cost_usd) ELSE \"usage\".total_cost_usd END",
"actual_total_cost_usd = CASE WHEN \"usage\".billing_status = 'pending' AND EXCLUDED.status IN ('completed', 'failed', 'cancelled') THEN GREATEST(\"usage\".actual_total_cost_usd, EXCLUDED.actual_total_cost_usd) ELSE \"usage\".actual_total_cost_usd END",
] {
assert!(
super::UPSERT_SQL.contains(assignment),
"missing deprecated mirror no-op assignment: {assignment}"
"missing terminal mirror assignment: {assignment}"
);
}
}
#[test]
fn usage_sql_binds_usage_mirror_values_for_terminal_upserts() {
let source = include_str!("mod.rs");
for binding in [
".bind(usage.input_tokens.map(to_i32).transpose()?)",
".bind(usage.output_tokens.map(to_i32).transpose()?)",
".bind(usage.total_tokens.map(to_i32).transpose()?)",
".bind(usage.cache_creation_input_tokens.map(to_i32).transpose()?)",
".bind(usage.cache_read_input_tokens.map(to_i32).transpose()?)",
".bind(usage.cache_creation_cost_usd)",
".bind(usage.cache_read_cost_usd)",
".bind(usage.total_cost_usd)",
".bind(usage.actual_total_cost_usd)",
] {
assert!(
source.contains(binding),
"missing usage upsert bind: {binding}"
);
}
assert!(!super::UPSERT_SQL.contains("EXCLUDED.input_tokens"));
assert!(!super::UPSERT_SQL.contains("EXCLUDED.total_cost_usd"));
}
#[test]