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
@@ -195,3 +195,53 @@ pub(crate) fn build_local_execution_report_context(
object.extend(parts.extra_fields);
Value::Object(object)
}
pub(crate) fn provider_stream_event_api_format_for_provider_type(
provider_type: &str,
) -> Option<&'static str> {
match provider_type.trim().to_ascii_lowercase().as_str() {
"codex" => Some("openai:cli"),
_ => None,
}
}
pub(crate) fn insert_provider_stream_event_api_format(
extra_fields: &mut Map<String, Value>,
provider_type: &str,
) {
if let Some(api_format) = provider_stream_event_api_format_for_provider_type(provider_type) {
extra_fields.insert(
"provider_stream_event_api_format".to_string(),
Value::String(api_format.to_string()),
);
}
}
#[cfg(test)]
mod tests {
use super::provider_stream_event_api_format_for_provider_type;
#[test]
fn codex_provider_uses_openai_cli_stream_event_format() {
assert_eq!(
provider_stream_event_api_format_for_provider_type("codex"),
Some("openai:cli")
);
assert_eq!(
provider_stream_event_api_format_for_provider_type("CODEX"),
Some("openai:cli")
);
}
#[test]
fn ordinary_providers_do_not_override_stream_event_format() {
assert_eq!(
provider_stream_event_api_format_for_provider_type("openai"),
None
);
assert_eq!(
provider_stream_event_api_format_for_provider_type("anthropic"),
None
);
}
}
@@ -3,7 +3,8 @@ use crate::ai_pipeline::planner::payload_metadata::{
build_local_execution_decision_response, LocalExecutionDecisionResponseParts,
};
use crate::ai_pipeline::planner::report_context::{
build_local_execution_report_context, LocalExecutionReportContextParts,
build_local_execution_report_context, insert_provider_stream_event_api_format,
LocalExecutionReportContextParts,
};
use crate::ai_pipeline::transport::{
resolve_transport_execution_timeouts, resolve_transport_tls_profile,
@@ -75,6 +76,10 @@ pub(crate) async fn maybe_build_local_openai_chat_decision_payload_for_candidate
serde_json::Value::String(envelope_name.to_string()),
);
}
insert_provider_stream_event_api_format(
&mut extra_fields,
resolved.transport.provider.provider_type.as_str(),
);
let report_context = append_local_failover_policy_to_value(
append_execution_contract_fields_to_value(
build_local_execution_report_context(LocalExecutionReportContextParts {
@@ -6,7 +6,8 @@ use crate::ai_pipeline::planner::payload_metadata::{
build_local_execution_decision_response, LocalExecutionDecisionResponseParts,
};
use crate::ai_pipeline::planner::report_context::{
build_local_execution_report_context, LocalExecutionReportContextParts,
build_local_execution_report_context, insert_provider_stream_event_api_format,
LocalExecutionReportContextParts,
};
use crate::ai_pipeline::planner::spec_metadata::local_openai_cli_spec_metadata;
use crate::ai_pipeline::transport::{
@@ -73,6 +74,10 @@ pub(crate) async fn maybe_build_local_openai_cli_decision_payload_for_candidate(
if let Some(envelope_name) = resolved.envelope_name {
extra_fields.insert("envelope_name".to_string(), json!(envelope_name));
}
insert_provider_stream_event_api_format(
&mut extra_fields,
resolved.transport.provider.provider_type.as_str(),
);
let report_context = append_local_failover_policy_to_value(
append_execution_contract_fields_to_value(
build_local_execution_report_context(LocalExecutionReportContextParts {