mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
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:
@@ -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 {
|
||||
|
||||
@@ -264,7 +264,10 @@ fn merge_stream_terminal_summary(
|
||||
return Some(observed);
|
||||
};
|
||||
|
||||
if current_summary.standardized_usage.is_none() {
|
||||
if should_replace_stream_usage(
|
||||
current_summary.standardized_usage.as_ref(),
|
||||
observed.standardized_usage.as_ref(),
|
||||
) {
|
||||
current_summary.standardized_usage = observed.standardized_usage;
|
||||
}
|
||||
if current_summary.finish_reason.is_none() {
|
||||
@@ -284,6 +287,20 @@ fn merge_stream_terminal_summary(
|
||||
current
|
||||
}
|
||||
|
||||
fn should_replace_stream_usage(
|
||||
current: Option<&aether_contracts::StandardizedUsage>,
|
||||
observed: Option<&aether_contracts::StandardizedUsage>,
|
||||
) -> bool {
|
||||
let Some(observed) = observed else {
|
||||
return false;
|
||||
};
|
||||
let Some(current) = current else {
|
||||
return true;
|
||||
};
|
||||
|
||||
observed.is_more_complete_than(current)
|
||||
}
|
||||
|
||||
async fn execute_in_process_stream(
|
||||
state: &AppState,
|
||||
plan: &ExecutionPlan,
|
||||
@@ -2135,7 +2152,10 @@ mod tests {
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use aether_contracts::{ExecutionPlan, ExecutionTimeouts, RequestBody};
|
||||
use aether_contracts::{
|
||||
ExecutionPlan, ExecutionStreamTerminalSummary, ExecutionTimeouts, RequestBody,
|
||||
StandardizedUsage,
|
||||
};
|
||||
use aether_data::repository::usage::InMemoryUsageReadRepository;
|
||||
use aether_data_contracts::repository::usage::UsageReadRepository;
|
||||
use aether_usage_runtime::UsageRuntimeConfig;
|
||||
@@ -2149,8 +2169,8 @@ mod tests {
|
||||
use tokio::sync::{watch, Notify};
|
||||
|
||||
use super::{
|
||||
execute_execution_runtime_stream, should_probe_success_failover_before_stream,
|
||||
should_skip_direct_finalize_prefetch,
|
||||
execute_execution_runtime_stream, merge_stream_terminal_summary,
|
||||
should_probe_success_failover_before_stream, should_skip_direct_finalize_prefetch,
|
||||
};
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::tunnel::{tunnel_protocol, TunnelProxyConn};
|
||||
@@ -2178,6 +2198,39 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_stream_terminal_summary_prefers_more_complete_observed_usage() {
|
||||
let mut runtime_usage = StandardizedUsage::new();
|
||||
runtime_usage.output_tokens = 137;
|
||||
let mut observed_usage = StandardizedUsage::new();
|
||||
observed_usage.input_tokens = 26;
|
||||
observed_usage.output_tokens = 137;
|
||||
|
||||
let merged = merge_stream_terminal_summary(
|
||||
Some(ExecutionStreamTerminalSummary {
|
||||
standardized_usage: Some(runtime_usage),
|
||||
model: Some("gpt-5.5".to_string()),
|
||||
..ExecutionStreamTerminalSummary::default()
|
||||
}),
|
||||
Some(ExecutionStreamTerminalSummary {
|
||||
standardized_usage: Some(observed_usage),
|
||||
response_id: Some("resp_123".to_string()),
|
||||
observed_finish: true,
|
||||
..ExecutionStreamTerminalSummary::default()
|
||||
}),
|
||||
)
|
||||
.expect("summary should merge");
|
||||
let usage = merged
|
||||
.standardized_usage
|
||||
.expect("merged usage should exist");
|
||||
|
||||
assert_eq!(usage.input_tokens, 26);
|
||||
assert_eq!(usage.output_tokens, 137);
|
||||
assert_eq!(merged.model.as_deref(), Some("gpt-5.5"));
|
||||
assert_eq!(merged.response_id.as_deref(), Some("resp_123"));
|
||||
assert!(merged.observed_finish);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_prefetch_for_same_format_passthrough_event_streams() {
|
||||
assert!(should_skip_direct_finalize_prefetch(
|
||||
|
||||
@@ -543,6 +543,8 @@ async fn gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_r
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
}
|
||||
let stored_usage = stored_usage.expect("usage should be recorded");
|
||||
assert_eq!(stored_usage.input_tokens, 1);
|
||||
assert_eq!(stored_usage.output_tokens, 2);
|
||||
assert_eq!(stored_usage.total_tokens, 3);
|
||||
assert!(stored_usage.request_body.is_none());
|
||||
assert!(stored_usage.provider_request_body.is_none());
|
||||
|
||||
Reference in New Issue
Block a user