refactor: 拆分 gateway 单体为独立 crate,新增 systemd 部署方案

将 gateway 内部的 model-fetch、provider-transport、scheduler-core、
usage-runtime、video-tasks-core 模块提取为独立 crate;重构 gateway
内部模块结构(state/router/cache/data/query 等);移除大量遗留模块
文件;新增 systemd 二进制部署骨架及相关文档;更新前端 usage 相关
API 和组件。
This commit is contained in:
fawney19
2026-04-05 20:23:16 +08:00
parent cbc811f6ce
commit 763ff03a7b
777 changed files with 42654 additions and 21464 deletions
@@ -251,9 +251,7 @@ pub(crate) fn append_execution_contract_fields_to_value(
#[cfg(test)]
mod tests {
use super::{
append_execution_contract_fields_to_value, ConversionMode, ExecutionStrategy,
};
use super::{append_execution_contract_fields_to_value, ConversionMode, ExecutionStrategy};
use serde_json::json;
#[test]
@@ -3,7 +3,7 @@ use std::io::Error as IoError;
use aether_contracts::StreamFrame;
use axum::body::Bytes;
use crate::gateway::GatewayError;
use crate::GatewayError;
pub(crate) fn encode_stream_frame_ndjson(frame: &StreamFrame) -> Result<Bytes, IoError> {
let mut raw = serde_json::to_vec(frame).map_err(|err| IoError::other(err.to_string()))?;
@@ -1,7 +1,7 @@
use aether_contracts::{ExecutionPlan, ExecutionResult};
use crate::gateway::constants::TRACE_ID_HEADER;
use crate::gateway::{AppState, GatewayError};
use crate::constants::TRACE_ID_HEADER;
use crate::{AppState, GatewayError};
fn build_remote_execution_runtime_request(
state: &AppState,
@@ -17,7 +17,7 @@ use axum::{Json, Router};
use serde_json::json;
use thiserror::Error;
use crate::gateway::execution_runtime::{
use crate::execution_runtime::{
build_direct_execution_frame_stream, DirectSyncExecutionRuntime, ExecutionRuntimeTransportError,
};
@@ -9,11 +9,12 @@ use serde_json::json;
use tokio_util::codec::{FramedRead, LinesCodec};
use tracing::warn;
use crate::gateway::api::response::build_client_response_from_parts;
use crate::gateway::execution_runtime::ndjson::decode_stream_frame_ndjson;
use crate::gateway::execution_runtime::submission::{has_nested_error, strip_utf8_bom_and_ws};
use crate::gateway::{GatewayControlDecision, GatewayError};
use crate::gateway::{
use crate::api::response::build_client_response_from_parts;
use crate::control::GatewayControlDecision;
use crate::execution_runtime::ndjson::decode_stream_frame_ndjson;
use crate::execution_runtime::submission::{has_nested_error, strip_utf8_bom_and_ws};
use crate::GatewayError;
use crate::{
GEMINI_FILES_DOWNLOAD_PLAN_KIND, MAX_ERROR_BODY_BYTES, MAX_STREAM_PREFETCH_FRAMES,
OPENAI_VIDEO_CONTENT_PLAN_KIND,
};
@@ -23,35 +23,35 @@ use self::execution_failures::{
build_stream_failure_from_execution_error, build_stream_failure_report,
handle_prefetch_stream_failure, submit_midstream_stream_failure, StreamFailureReport,
};
use crate::gateway::ai_pipeline::runtime::{
use crate::ai_pipeline::adaptation::private_envelope::{
maybe_build_provider_private_stream_normalizer, normalize_provider_private_report_context,
};
use crate::gateway::api::response::{
use crate::ai_pipeline::finalize::maybe_build_stream_response_rewriter;
use crate::api::response::{
attach_control_metadata_headers, build_client_response, build_client_response_from_parts,
};
use crate::gateway::constants::{CONTROL_CANDIDATE_ID_HEADER, CONTROL_REQUEST_ID_HEADER};
use crate::gateway::execution_runtime::build_direct_execution_frame_stream;
use crate::constants::{CONTROL_CANDIDATE_ID_HEADER, CONTROL_REQUEST_ID_HEADER};
use crate::control::GatewayControlDecision;
use crate::execution_runtime::build_direct_execution_frame_stream;
#[cfg(test)]
use crate::gateway::execution_runtime::remote_compat::post_stream_plan_to_remote_execution_runtime;
use crate::gateway::execution_runtime::submission::{
use crate::execution_runtime::remote_compat::post_stream_plan_to_remote_execution_runtime;
use crate::execution_runtime::submission::{
resolve_core_error_background_report_kind, submit_local_core_error_or_sync_finalize,
};
use crate::gateway::execution_runtime::transport::{
use crate::execution_runtime::transport::{
DirectSyncExecutionRuntime, DirectUpstreamStreamExecution,
};
use crate::gateway::scheduler::{
use crate::execution_runtime::{MAX_STREAM_PREFETCH_BYTES, MAX_STREAM_PREFETCH_FRAMES};
use crate::scheduler::{
current_unix_secs as current_request_candidate_unix_secs,
ensure_execution_request_candidate_slot, record_local_request_candidate_status,
resolve_core_stream_direct_finalize_report_kind,
resolve_core_stream_error_finalize_report_kind, should_fallback_to_control_stream,
should_retry_next_local_candidate_stream,
};
use crate::gateway::usage::submit_stream_report;
use crate::gateway::{
maybe_build_stream_response_rewriter, AppState, GatewayControlDecision, GatewayError,
GatewayStreamReportRequest, GatewaySyncReportRequest, MAX_STREAM_PREFETCH_BYTES,
MAX_STREAM_PREFETCH_FRAMES,
};
use crate::usage::submit_stream_report;
use crate::usage::{GatewayStreamReportRequest, GatewaySyncReportRequest};
use crate::{AppState, GatewayError};
#[allow(clippy::too_many_arguments)] // internal function, grouping would add unnecessary indirection
pub(crate) async fn execute_execution_runtime_stream(
@@ -73,6 +73,8 @@ pub(crate) async fn execute_execution_runtime_stream(
Ok(execution) => execution,
Err(err) => {
warn!(
event_name = "stream_execution_runtime_unavailable",
log_type = "ops",
trace_id = %trace_id,
request_id = %plan.request_id,
candidate_id = ?plan.candidate_id,
@@ -108,6 +110,8 @@ pub(crate) async fn execute_execution_runtime_stream(
Ok(execution) => execution,
Err(err) => {
warn!(
event_name = "stream_execution_runtime_unavailable",
log_type = "ops",
trace_id = %trace_id,
request_id = %plan.request_id,
candidate_id = ?plan.candidate_id,
@@ -142,7 +146,11 @@ pub(crate) async fn execute_execution_runtime_stream(
Ok(response) => response,
Err(err) => {
warn!(
event_name = "stream_execution_runtime_remote_unavailable",
log_type = "ops",
trace_id = %trace_id,
request_id = %plan.request_id,
candidate_id = ?plan.candidate_id,
error = ?err,
"gateway remote execution runtime stream unavailable"
);
@@ -239,6 +247,8 @@ async fn execute_stream_from_frame_stream(
)
.await;
warn!(
event_name = "local_stream_candidate_retry_scheduled",
log_type = "event",
trace_id = %trace_id,
request_id,
status_code,
@@ -566,7 +576,15 @@ async fn execute_stream_from_frame_stream(
break;
}
StreamFramePayload::Error { error } => {
warn!(trace_id = %trace_id, error = %error.message, "execution runtime stream emitted error frame during prefetch");
warn!(
event_name = "stream_execution_prefetch_error_frame",
log_type = "ops",
trace_id = %trace_id,
request_id,
candidate_id = ?candidate_id,
error = %error.message,
"execution runtime stream emitted error frame during prefetch"
);
return handle_prefetch_stream_failure(
state,
trace_id,
@@ -634,6 +652,8 @@ async fn execute_stream_from_frame_stream(
let initial_reached_eof = reached_eof;
let direct_stream_finalize_kind_owned = direct_stream_finalize_kind.clone();
let candidate_started_unix_secs_for_report = candidate_started_unix_secs;
let request_id_for_report = request_id.to_string();
let candidate_id_for_report = candidate_id.map(ToOwned::to_owned);
tokio::spawn(async move {
let mut provider_buffered_body = provider_prefetched_body_for_report;
let mut buffered_body = prefetched_body_for_report;
@@ -647,7 +667,15 @@ async fn execute_stream_from_frame_stream(
let next_frame = match read_next_frame(&mut lines).await {
Ok(frame) => frame,
Err(err) => {
warn!(trace_id = %trace_id_owned, error = ?err, "gateway failed to decode execution runtime stream frame");
warn!(
event_name = "stream_execution_frame_decode_failed",
log_type = "ops",
trace_id = %trace_id_owned,
request_id = %request_id_for_report,
candidate_id = ?candidate_id_for_report.as_deref(),
error = ?err,
"gateway failed to decode execution runtime stream frame"
);
terminal_failure = Some(build_stream_failure_report(
"execution_runtime_stream_frame_decode_error",
format!("failed to decode execution runtime stream frame: {err:?}"),
@@ -665,7 +693,15 @@ async fn execute_stream_from_frame_stream(
match base64::engine::general_purpose::STANDARD.decode(chunk_b64) {
Ok(decoded) => decoded,
Err(err) => {
warn!(trace_id = %trace_id_owned, error = %err, "gateway failed to decode execution runtime chunk");
warn!(
event_name = "stream_execution_chunk_decode_failed",
log_type = "ops",
trace_id = %trace_id_owned,
request_id = %request_id_for_report,
candidate_id = ?candidate_id_for_report.as_deref(),
error = %err,
"gateway failed to decode execution runtime chunk"
);
terminal_failure = Some(build_stream_failure_report(
"execution_runtime_stream_chunk_decode_error",
format!("failed to decode execution runtime stream chunk: {err}"),
@@ -691,7 +727,15 @@ async fn execute_stream_from_frame_stream(
match normalizer.push_chunk(&chunk) {
Ok(normalized_chunk) => normalized_chunk,
Err(err) => {
warn!(trace_id = %trace_id_owned, error = ?err, "gateway failed to normalize execution runtime stream chunk");
warn!(
event_name = "stream_execution_chunk_normalize_failed",
log_type = "ops",
trace_id = %trace_id_owned,
request_id = %request_id_for_report,
candidate_id = ?candidate_id_for_report.as_deref(),
error = ?err,
"gateway failed to normalize execution runtime stream chunk"
);
terminal_failure = Some(build_stream_failure_report(
"execution_runtime_stream_rewrite_error",
format!("failed to normalize execution runtime stream chunk: {err:?}"),
@@ -708,7 +752,15 @@ async fn execute_stream_from_frame_stream(
match rewriter.push_chunk(&normalized_chunk) {
Ok(rewritten_chunk) => rewritten_chunk,
Err(err) => {
warn!(trace_id = %trace_id_owned, error = ?err, "gateway failed to rewrite execution runtime stream chunk");
warn!(
event_name = "stream_execution_chunk_rewrite_failed",
log_type = "ops",
trace_id = %trace_id_owned,
request_id = %request_id_for_report,
candidate_id = ?candidate_id_for_report.as_deref(),
error = ?err,
"gateway failed to rewrite execution runtime stream chunk"
);
terminal_failure = Some(build_stream_failure_report(
"execution_runtime_stream_rewrite_error",
format!("failed to rewrite execution runtime stream chunk: {err:?}"),
@@ -728,7 +780,11 @@ async fn execute_stream_from_frame_stream(
buffered_body.extend_from_slice(&rewritten_chunk);
if tx.send(Ok(Bytes::from(rewritten_chunk))).await.is_err() {
warn!(
event_name = "stream_execution_downstream_disconnected",
log_type = "ops",
trace_id = %trace_id_owned,
request_id = %request_id_for_report,
candidate_id = ?candidate_id_for_report.as_deref(),
"gateway stream downstream dropped; stopping execution runtime stream forwarding"
);
downstream_dropped = true;
@@ -744,7 +800,15 @@ async fn execute_stream_from_frame_stream(
break;
}
StreamFramePayload::Error { error } => {
warn!(trace_id = %trace_id_owned, error = %error.message, "execution runtime stream emitted error frame");
warn!(
event_name = "stream_execution_error_frame",
log_type = "ops",
trace_id = %trace_id_owned,
request_id = %request_id_for_report,
candidate_id = ?candidate_id_for_report.as_deref(),
error = %error.message,
"execution runtime stream emitted error frame"
);
terminal_failure = Some(build_stream_failure_from_execution_error(&error));
break;
}
@@ -755,6 +819,10 @@ async fn execute_stream_from_frame_stream(
if downstream_dropped {
debug!(
event_name = "execution_runtime_stream_flush_skipped",
log_type = "debug",
debug_context = "redacted",
stream_status = "downstream_disconnected",
trace_id = %trace_id_owned,
"gateway skipped local stream flush after downstream disconnect"
);
@@ -767,7 +835,15 @@ async fn execute_stream_from_frame_stream(
match rewriter.push_chunk(&normalized_chunk) {
Ok(rewritten_chunk) => rewritten_chunk,
Err(err) => {
warn!(trace_id = %trace_id_owned, error = ?err, "gateway failed to rewrite normalized private stream chunk during flush");
warn!(
event_name = "stream_execution_normalized_flush_rewrite_failed",
log_type = "ops",
trace_id = %trace_id_owned,
request_id = %request_id_for_report,
candidate_id = ?candidate_id_for_report.as_deref(),
error = ?err,
"gateway failed to rewrite normalized private stream chunk during flush"
);
terminal_failure.get_or_insert_with(|| {
build_stream_failure_report(
"execution_runtime_stream_rewrite_flush_error",
@@ -785,7 +861,11 @@ async fn execute_stream_from_frame_stream(
buffered_body.extend_from_slice(&rewritten_chunk);
if tx.send(Ok(Bytes::from(rewritten_chunk))).await.is_err() {
warn!(
event_name = "stream_execution_downstream_flush_disconnected",
log_type = "ops",
trace_id = %trace_id_owned,
request_id = %request_id_for_report,
candidate_id = ?candidate_id_for_report.as_deref(),
"gateway stream downstream dropped while flushing private stream normalization"
);
downstream_dropped = true;
@@ -794,7 +874,15 @@ async fn execute_stream_from_frame_stream(
}
Ok(_) => {}
Err(err) => {
warn!(trace_id = %trace_id_owned, error = ?err, "gateway failed to flush private stream normalization");
warn!(
event_name = "stream_execution_normalization_flush_failed",
log_type = "ops",
trace_id = %trace_id_owned,
request_id = %request_id_for_report,
candidate_id = ?candidate_id_for_report.as_deref(),
error = ?err,
"gateway failed to flush private stream normalization"
);
terminal_failure.get_or_insert_with(|| {
build_stream_failure_report(
"execution_runtime_stream_rewrite_flush_error",
@@ -812,7 +900,11 @@ async fn execute_stream_from_frame_stream(
buffered_body.extend_from_slice(&flushed_chunk);
if tx.send(Ok(Bytes::from(flushed_chunk))).await.is_err() {
warn!(
event_name = "stream_execution_downstream_rewrite_flush_disconnected",
log_type = "ops",
trace_id = %trace_id_owned,
request_id = %request_id_for_report,
candidate_id = ?candidate_id_for_report.as_deref(),
"gateway stream downstream dropped while flushing local stream rewrite"
);
downstream_dropped = true;
@@ -820,7 +912,15 @@ async fn execute_stream_from_frame_stream(
}
Ok(_) => {}
Err(err) => {
warn!(trace_id = %trace_id_owned, error = ?err, "gateway failed to flush local stream rewrite");
warn!(
event_name = "stream_execution_rewrite_flush_failed",
log_type = "ops",
trace_id = %trace_id_owned,
request_id = %request_id_for_report,
candidate_id = ?candidate_id_for_report.as_deref(),
error = ?err,
"gateway failed to flush local stream rewrite"
);
terminal_failure.get_or_insert_with(|| {
build_stream_failure_report(
"execution_runtime_stream_rewrite_flush_error",
@@ -838,6 +938,11 @@ async fn execute_stream_from_frame_stream(
if downstream_dropped {
debug!(
event_name = "execution_runtime_stream_report_skipped",
log_type = "debug",
debug_context = "redacted",
stream_status = "downstream_disconnected",
status_code = 499_u16,
trace_id = %trace_id_owned,
"gateway skipped stream report because downstream disconnected before completion"
);
@@ -939,7 +1044,16 @@ async fn execute_stream_from_frame_stream(
report.report_kind = report_kind;
if let Err(err) = submit_stream_report(&state_for_report, &trace_id_owned, report).await
{
warn!(trace_id = %trace_id_owned, error = ?err, "gateway failed to submit stream execution report");
warn!(
event_name = "execution_report_submit_failed",
log_type = "ops",
trace_id = %trace_id_owned,
request_id = %request_id_for_report,
candidate_id = ?candidate_id_for_report.as_deref(),
report_scope = "stream",
error = ?err,
"gateway failed to submit stream execution report"
);
}
}
});
@@ -5,16 +5,17 @@ use base64::Engine as _;
use serde_json::{Map, Value};
use tracing::warn;
use crate::gateway::api::response::attach_control_metadata_headers;
use crate::gateway::execution_runtime::submission::{
use crate::api::response::attach_control_metadata_headers;
use crate::control::GatewayControlDecision;
use crate::execution_runtime::submission::{
resolve_core_error_background_report_kind, submit_local_core_error_or_sync_finalize,
};
use crate::gateway::scheduler::{
use crate::scheduler::{
current_unix_secs as current_request_candidate_unix_secs,
record_report_request_candidate_status,
};
use crate::gateway::usage::submit_sync_report;
use crate::gateway::{AppState, GatewayControlDecision, GatewayError, GatewaySyncReportRequest};
use crate::usage::submit_sync_report;
use crate::{usage::GatewaySyncReportRequest, AppState, GatewayError};
#[derive(Debug, Clone)]
pub(super) struct StreamFailureReport {
@@ -220,7 +221,12 @@ pub(super) async fn submit_midstream_stream_failure(
.await;
if let Err(err) = submit_sync_report(state, trace_id, payload).await {
warn!(
event_name = "execution_report_submit_failed",
log_type = "ops",
trace_id = %trace_id,
request_id = %plan.request_id,
candidate_id = ?plan.candidate_id,
report_scope = "stream_failure",
error = ?err,
"gateway failed to submit sync execution report for terminal stream failure"
);
@@ -1,7 +1,8 @@
use axum::body::{Body, Bytes};
use axum::http::Response;
use crate::gateway::{AppState, GatewayControlDecision, GatewayError};
use crate::control::GatewayControlDecision;
use crate::{AppState, GatewayError};
mod error;
mod execution;
@@ -24,7 +25,7 @@ pub(crate) async fn maybe_execute_via_execution_runtime_stream(
if parts.method != http::Method::POST {
return Ok(None);
}
return crate::gateway::executor::maybe_execute_stream_local_path(
return crate::executor::maybe_execute_stream_local_path(
state, parts, body_bytes, trace_id, decision,
)
.await;
@@ -39,7 +40,7 @@ pub(crate) async fn maybe_execute_via_execution_runtime_stream(
{
return Ok(None);
}
crate::gateway::executor::maybe_execute_stream_local_path(
crate::executor::maybe_execute_stream_local_path(
state, parts, body_bytes, trace_id, decision,
)
.await
@@ -9,8 +9,8 @@ use axum::body::Bytes;
use base64::Engine as _;
use futures_util::{Stream, StreamExt};
use crate::gateway::execution_runtime::ndjson::encode_stream_frame_ndjson;
use crate::gateway::execution_runtime::DirectUpstreamStreamExecution;
use crate::execution_runtime::ndjson::encode_stream_frame_ndjson;
use crate::execution_runtime::DirectUpstreamStreamExecution;
pub(crate) fn build_direct_execution_frame_stream(
execution: DirectUpstreamStreamExecution,
@@ -41,10 +41,14 @@ pub(crate) fn build_direct_execution_frame_stream(
}
let mut upstream_bytes = 0u64;
let mut ttfb_ms = None;
let mut bytes_stream = response.bytes_stream();
while let Some(item) = bytes_stream.next().await {
match item {
Ok(chunk) => {
if ttfb_ms.is_none() {
ttfb_ms = Some(started_at.elapsed().as_millis() as u64);
}
upstream_bytes += chunk.len() as u64;
let frame = StreamFrame {
frame_type: StreamFrameType::Data,
@@ -91,7 +95,7 @@ pub(crate) fn build_direct_execution_frame_stream(
frame_type: StreamFrameType::Telemetry,
payload: StreamFramePayload::Telemetry {
telemetry: ExecutionTelemetry {
ttfb_ms: None,
ttfb_ms,
elapsed_ms: Some(started_at.elapsed().as_millis() as u64),
upstream_bytes: Some(upstream_bytes),
},
@@ -110,3 +114,107 @@ pub(crate) fn build_direct_execution_frame_stream(
}
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::convert::Infallible;
use std::time::Duration;
use aether_contracts::{ExecutionPlan, ExecutionTimeouts, RequestBody};
use async_stream::stream;
use axum::body::{Body, Bytes};
use axum::routing::post;
use axum::{http::header, http::HeaderValue, Router};
use futures_util::StreamExt;
use serde_json::Value;
use super::build_direct_execution_frame_stream;
use crate::execution_runtime::transport::DirectSyncExecutionRuntime;
#[tokio::test]
async fn direct_execution_frame_stream_reports_ttfb_after_first_upstream_chunk() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("listener should bind");
let addr = listener.local_addr().expect("local addr should resolve");
let app = Router::new().route(
"/chat",
post(|| async {
let stream = stream! {
tokio::time::sleep(Duration::from_millis(25)).await;
yield Ok::<Bytes, Infallible>(Bytes::from_static(b"data: hello\n\n"));
yield Ok::<Bytes, Infallible>(Bytes::from_static(b"data: [DONE]\n\n"));
};
let mut response = axum::http::Response::new(Body::from_stream(stream));
response.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_static("text/event-stream"),
);
response
}),
);
let server = tokio::spawn(async move {
axum::serve(listener, app)
.await
.expect("test server should run");
});
let execution = DirectSyncExecutionRuntime::new()
.execute_stream(ExecutionPlan {
request_id: "req-stream-ttfb-1".into(),
candidate_id: Some("cand-stream-ttfb-1".into()),
provider_name: Some("openai".into()),
provider_id: "prov-1".into(),
endpoint_id: "ep-1".into(),
key_id: "key-1".into(),
method: "POST".into(),
url: format!("http://{addr}/chat"),
headers: BTreeMap::from([("content-type".into(), "application/json".into())]),
content_type: Some("application/json".into()),
content_encoding: None,
body: RequestBody::from_json(serde_json::json!({"stream": true})),
stream: true,
client_api_format: "openai:chat".into(),
provider_api_format: "openai:chat".into(),
model_name: Some("gpt-5".into()),
proxy: None,
tls_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(5_000),
total_ms: Some(5_000),
..ExecutionTimeouts::default()
}),
})
.await
.expect("stream execution should succeed");
let frame_output = build_direct_execution_frame_stream(execution)
.map(|item| item.expect("frame should encode"))
.collect::<Vec<_>>()
.await
.into_iter()
.map(|bytes| String::from_utf8(bytes.to_vec()).expect("frame should be utf8"))
.collect::<String>();
server.abort();
let telemetry_ttfb_ms = frame_output
.lines()
.filter_map(|line| serde_json::from_str::<Value>(line).ok())
.find_map(|frame| {
(frame.get("type").and_then(Value::as_str) == Some("telemetry")).then(|| {
frame
.get("payload")
.and_then(|payload| payload.get("telemetry"))
.and_then(|telemetry| telemetry.get("ttfb_ms"))
.and_then(Value::as_u64)
})?
});
assert!(
telemetry_ttfb_ms.is_some_and(|value| value > 0),
"telemetry frame should include a measured ttfb"
);
}
}
@@ -1,14 +1,13 @@
use crate::gateway::ai_pipeline::adaptation::private_envelope::normalize_provider_private_response_value as unwrap_local_finalize_response_value;
use crate::gateway::ai_pipeline::conversion::{
use crate::ai_pipeline::adaptation::private_envelope::normalize_provider_private_response_value as unwrap_local_finalize_response_value;
use crate::ai_pipeline::conversion::{
build_core_error_body_for_client_format, core_error_background_report_kind,
core_error_default_client_api_format, is_core_error_finalize_kind, LocalCoreSyncErrorKind,
};
use crate::gateway::api::response::build_client_response_from_parts;
use crate::gateway::usage::spawn_sync_report;
use crate::gateway::{
maybe_compile_sync_finalize_response, AppState, GatewayControlDecision, GatewayError,
GatewaySyncReportRequest,
};
use crate::ai_pipeline::finalize::maybe_compile_sync_finalize_response;
use crate::api::response::build_client_response_from_parts;
use crate::control::GatewayControlDecision;
use crate::usage::spawn_sync_report;
use crate::{usage::GatewaySyncReportRequest, AppState, GatewayError};
use axum::body::Body;
use axum::http::Response;
use base64::Engine as _;
@@ -223,7 +222,7 @@ pub(crate) fn resolve_core_error_background_report_kind(report_kind: &str) -> Op
#[cfg(test)]
pub(crate) fn resolve_core_success_background_report_kind(report_kind: &str) -> Option<String> {
crate::gateway::ai_pipeline::conversion::core_success_background_report_kind(report_kind)
crate::ai_pipeline::conversion::core_success_background_report_kind(report_kind)
.map(ToOwned::to_owned)
}
@@ -437,6 +436,8 @@ pub(crate) async fn submit_local_core_error_or_sync_finalize(
response
} else {
warn!(
event_name = "local_core_finalize_fallback_raw_response_body",
log_type = "event",
trace_id = %trace_id,
report_kind = %payload.report_kind,
status_code = payload.status_code,
@@ -457,6 +458,8 @@ pub(crate) async fn submit_local_core_error_or_sync_finalize(
spawn_sync_report(state.clone(), trace_id.to_string(), report_payload);
} else {
warn!(
event_name = "local_core_finalize_missing_error_report_mapping",
log_type = "event",
trace_id = %trace_id,
report_kind = %payload.report_kind,
"gateway built local core finalize response without background error report mapping"
@@ -6,28 +6,27 @@ use axum::http::Response;
use base64::Engine as _;
use tracing::warn;
use crate::gateway::ai_pipeline::contracts::implicit_sync_finalize_report_kind;
use crate::gateway::api::response::{
use crate::ai_pipeline::contracts::implicit_sync_finalize_report_kind;
use crate::ai_pipeline::finalize::maybe_build_sync_finalize_outcome;
use crate::api::response::{
attach_control_metadata_headers, build_client_response, build_client_response_from_parts,
};
use crate::gateway::constants::{CONTROL_CANDIDATE_ID_HEADER, CONTROL_REQUEST_ID_HEADER};
use crate::constants::{CONTROL_CANDIDATE_ID_HEADER, CONTROL_REQUEST_ID_HEADER};
use crate::control::GatewayControlDecision;
#[cfg(test)]
use crate::gateway::execution_runtime::remote_compat::post_sync_plan_to_remote_execution_runtime;
use crate::gateway::execution_runtime::submission::submit_local_core_error_or_sync_finalize;
use crate::gateway::execution_runtime::transport::DirectSyncExecutionRuntime;
use crate::gateway::scheduler::{
use crate::execution_runtime::remote_compat::post_sync_plan_to_remote_execution_runtime;
use crate::execution_runtime::submission::submit_local_core_error_or_sync_finalize;
use crate::execution_runtime::transport::DirectSyncExecutionRuntime;
use crate::scheduler::{
current_unix_secs as current_request_candidate_unix_secs,
ensure_execution_request_candidate_slot, execution_error_details,
record_local_request_candidate_status, resolve_core_sync_error_finalize_report_kind,
should_fallback_to_control_sync, should_finalize_sync_response,
should_retry_next_local_candidate_sync,
};
use crate::gateway::usage::{spawn_sync_report, submit_sync_report};
use crate::gateway::video_tasks::VideoTaskSyncReportMode;
use crate::gateway::{
maybe_build_sync_finalize_outcome, AppState, GatewayControlDecision, GatewayError,
GatewaySyncReportRequest,
};
use crate::usage::{spawn_sync_report, submit_sync_report};
use crate::video_tasks::VideoTaskSyncReportMode;
use crate::{usage::GatewaySyncReportRequest, AppState, GatewayError};
#[path = "execution/policy.rs"]
mod policy;
@@ -43,7 +42,7 @@ pub(crate) use response::{
struct ImplicitSyncFinalizeOutcome {
payload: GatewaySyncReportRequest,
outcome: crate::gateway::ai_pipeline::finalize::LocalCoreSyncFinalizeOutcome,
outcome: crate::ai_pipeline::finalize::LocalCoreSyncFinalizeOutcome,
}
async fn record_sync_terminal_usage(
@@ -88,6 +87,8 @@ pub(crate) async fn execute_execution_runtime_sync(
Ok(result) => result,
Err(err) => {
warn!(
event_name = "sync_execution_runtime_unavailable",
log_type = "ops",
trace_id = %trace_id,
request_id = %plan_request_id,
candidate_id = ?plan_candidate_id,
@@ -111,6 +112,8 @@ pub(crate) async fn execute_execution_runtime_sync(
Ok(result) => result,
Err(err) => {
warn!(
event_name = "sync_execution_runtime_unavailable",
log_type = "ops",
trace_id = %trace_id,
request_id = %plan_request_id,
candidate_id = ?plan_candidate_id,
@@ -165,6 +168,8 @@ pub(crate) async fn execute_execution_runtime_sync(
)
.await;
warn!(
event_name = "local_sync_candidate_retry_scheduled",
log_type = "event",
trace_id = %trace_id,
request_id = %plan_request_id,
status_code = result.status_code,
@@ -280,6 +285,8 @@ pub(crate) async fn execute_execution_runtime_sync(
spawn_sync_report(state.clone(), trace_id.to_string(), report_payload);
} else {
warn!(
event_name = "local_core_finalize_missing_success_report_mapping",
log_type = "event",
trace_id = %trace_id,
report_kind = %implicit_finalize.payload.report_kind,
"gateway implicit local core finalize produced response without background success report mapping"
@@ -316,6 +323,8 @@ pub(crate) async fn execute_execution_runtime_sync(
spawn_sync_report(state.clone(), trace_id.to_string(), report_payload);
} else {
warn!(
event_name = "local_core_finalize_missing_success_report_mapping",
log_type = "event",
trace_id = %trace_id,
report_kind = %payload.report_kind,
"gateway local core finalize produced response without background success report mapping"
@@ -366,7 +375,7 @@ pub(crate) async fn execute_execution_runtime_sync(
resolve_local_sync_success_background_report_kind(payload.report_kind.as_str())
{
let mut report_payload = payload.clone();
report_payload.report_kind = success_report_kind;
report_payload.report_kind = success_report_kind.to_string();
report_payload
} else {
payload.clone()
@@ -391,11 +400,15 @@ pub(crate) async fn execute_execution_runtime_sync(
resolve_local_sync_success_background_report_kind(payload.report_kind.as_str())
{
let mut report_payload = usage_payload;
report_payload.report_kind = success_report_kind;
report_payload.report_kind = success_report_kind.to_string();
spawn_sync_report(state.clone(), trace_id.to_string(), report_payload);
} else {
warn!(
event_name = "local_video_finalize_missing_success_report_mapping",
log_type = "ops",
trace_id = %trace_id,
request_id = request_id.unwrap_or("-"),
candidate_id = ?candidate_id,
report_kind = %payload.report_kind,
"gateway local video finalize produced response without background success report mapping"
);
@@ -413,7 +426,7 @@ pub(crate) async fn execute_execution_runtime_sync(
resolve_local_sync_error_background_report_kind(payload.report_kind.as_str())
{
let mut report_payload = payload.clone();
report_payload.report_kind = error_report_kind;
report_payload.report_kind = error_report_kind.to_string();
report_payload
} else {
payload.clone()
@@ -429,11 +442,15 @@ pub(crate) async fn execute_execution_runtime_sync(
resolve_local_sync_error_background_report_kind(payload.report_kind.as_str())
{
let mut report_payload = usage_payload;
report_payload.report_kind = error_report_kind;
report_payload.report_kind = error_report_kind.to_string();
spawn_sync_report(state.clone(), trace_id.to_string(), report_payload);
} else {
warn!(
event_name = "local_video_finalize_missing_error_report_mapping",
log_type = "ops",
trace_id = %trace_id,
request_id = request_id.unwrap_or("-"),
candidate_id = ?candidate_id,
report_kind = %payload.report_kind,
"gateway local video finalize produced response without background error report mapping"
);
@@ -560,6 +577,8 @@ async fn execute_sync_via_remote_execution_runtime(
Ok(response) => response,
Err(err) => {
warn!(
event_name = "sync_execution_runtime_remote_unavailable",
log_type = "ops",
trace_id = %trace_id,
request_id = %plan_request_id,
candidate_id = ?plan_candidate_id,
@@ -3,7 +3,7 @@ use std::collections::BTreeMap;
use aether_contracts::ExecutionResult;
use base64::Engine as _;
use crate::gateway::GatewayError;
use crate::GatewayError;
type DecodedBody = (Vec<u8>, Option<serde_json::Value>, Option<String>);
@@ -5,10 +5,17 @@ use axum::body::Body;
use axum::http::Response;
use serde_json::json;
use crate::gateway::api::response::build_client_response_from_parts;
use crate::gateway::video_tasks::{LocalVideoTaskSnapshot, VideoTaskSyncReportMode};
use crate::gateway::VideoTaskService;
use crate::gateway::{GatewayControlDecision, GatewayError, GatewaySyncReportRequest};
use crate::api::response::build_client_response_from_parts;
use crate::async_task::VideoTaskService;
use crate::control::GatewayControlDecision;
use crate::video_tasks::{
build_local_sync_finalize_read_response, LocalVideoTaskSnapshot, VideoTaskSyncReportMode,
};
pub(crate) use crate::video_tasks::{
resolve_local_sync_error_background_report_kind,
resolve_local_sync_success_background_report_kind,
};
use crate::{usage::GatewaySyncReportRequest, GatewayError};
pub(crate) struct LocalVideoSyncSuccessOutcome {
pub(crate) response: Response<Body>,
@@ -104,47 +111,22 @@ pub(crate) fn maybe_build_local_sync_finalize_response(
decision: &GatewayControlDecision,
payload: &GatewaySyncReportRequest,
) -> Result<Option<Response<Body>>, GatewayError> {
let (status_code, body_json) = match payload.report_kind.as_str() {
"openai_video_delete_sync_finalize" => {
if payload.status_code >= 400 && payload.status_code != 404 {
return Ok(None);
}
let Some(task_id) = payload
.report_context
.as_ref()
.and_then(|value| value.get("task_id"))
.and_then(|value| value.as_str())
.map(str::trim)
.filter(|value| !value.is_empty())
else {
return Ok(None);
};
(
http::StatusCode::OK,
json!({
"id": task_id,
"object": "video",
"deleted": true,
}),
)
}
"openai_video_cancel_sync_finalize" | "gemini_video_cancel_sync_finalize" => {
if payload.status_code >= 400 {
return Ok(None);
}
(http::StatusCode::OK, json!({}))
}
_ => return Ok(None),
let Some(read_response) = build_local_sync_finalize_read_response(
payload.report_kind.as_str(),
payload.status_code,
payload.report_context.as_ref(),
) else {
return Ok(None);
};
let body_bytes =
serde_json::to_vec(&body_json).map_err(|err| GatewayError::Internal(err.to_string()))?;
let body_bytes = serde_json::to_vec(&read_response.body_json)
.map_err(|err| GatewayError::Internal(err.to_string()))?;
let mut headers = BTreeMap::new();
headers.insert("content-type".to_string(), "application/json".to_string());
headers.insert("content-length".to_string(), body_bytes.len().to_string());
Ok(Some(build_client_response_from_parts(
status_code.as_u16(),
read_response.status_code,
&headers,
Body::from(body_bytes),
trace_id,
@@ -157,15 +139,7 @@ pub(crate) fn maybe_build_local_video_error_response(
decision: &GatewayControlDecision,
payload: &GatewaySyncReportRequest,
) -> Result<Option<Response<Body>>, GatewayError> {
if !matches!(
payload.report_kind.as_str(),
"openai_video_create_sync_finalize"
| "openai_video_remix_sync_finalize"
| "gemini_video_create_sync_finalize"
| "openai_video_delete_sync_finalize"
| "openai_video_cancel_sync_finalize"
| "gemini_video_cancel_sync_finalize"
) {
if resolve_local_sync_error_background_report_kind(payload.report_kind.as_str()).is_none() {
return Ok(None);
}
@@ -191,28 +165,3 @@ pub(crate) fn maybe_build_local_video_error_response(
Some(decision),
)?))
}
pub(crate) fn resolve_local_sync_success_background_report_kind(
report_kind: &str,
) -> Option<String> {
let mapped = match report_kind {
"openai_video_delete_sync_finalize" => "openai_video_delete_sync_success",
"openai_video_cancel_sync_finalize" => "openai_video_cancel_sync_success",
"gemini_video_cancel_sync_finalize" => "gemini_video_cancel_sync_success",
_ => return None,
};
Some(mapped.to_string())
}
pub(crate) fn resolve_local_sync_error_background_report_kind(report_kind: &str) -> Option<String> {
let mapped = match report_kind {
"openai_video_create_sync_finalize" => "openai_video_create_sync_error",
"openai_video_remix_sync_finalize" => "openai_video_remix_sync_error",
"gemini_video_create_sync_finalize" => "gemini_video_create_sync_error",
"openai_video_delete_sync_finalize" => "openai_video_delete_sync_error",
"openai_video_cancel_sync_finalize" => "openai_video_cancel_sync_error",
"gemini_video_cancel_sync_finalize" => "gemini_video_cancel_sync_error",
_ => return None,
};
Some(mapped.to_string())
}
@@ -1,7 +1,8 @@
use axum::body::{Body, Bytes};
use axum::http::Response;
use crate::gateway::{AppState, GatewayControlDecision, GatewayError};
use crate::control::GatewayControlDecision;
use crate::{AppState, GatewayError};
mod execution;
@@ -30,7 +31,7 @@ pub(crate) async fn maybe_execute_via_execution_runtime_sync(
if parts.method != http::Method::POST {
return Ok(None);
}
return crate::gateway::executor::maybe_execute_sync_local_path(
return crate::executor::maybe_execute_sync_local_path(
state, parts, body_bytes, trace_id, decision,
)
.await;
@@ -45,7 +46,7 @@ pub(crate) async fn maybe_execute_via_execution_runtime_sync(
{
return Ok(None);
}
crate::gateway::executor::maybe_execute_sync_local_path(
crate::executor::maybe_execute_sync_local_path(
state, parts, body_bytes, trace_id, decision,
)
.await
@@ -2,22 +2,23 @@ use aether_contracts::{ExecutionPlan, RequestBody};
use axum::http::Request;
use serde_json::json;
use crate::gateway::ai_pipeline::planner::plan_builders::{
use crate::ai_pipeline::contracts::GatewayControlSyncDecisionResponse;
use crate::ai_pipeline::planner::plan_builders::{
build_gemini_stream_plan_from_decision, build_gemini_sync_plan_from_decision,
build_openai_cli_stream_plan_from_decision, build_openai_cli_sync_plan_from_decision,
build_passthrough_sync_plan_from_decision, build_standard_stream_plan_from_decision,
build_standard_sync_plan_from_decision,
};
use crate::gateway::execution_runtime::submission::{
use crate::execution_runtime::submission::{
build_best_effort_local_core_error_body, resolve_core_error_background_report_kind,
resolve_core_success_background_report_kind,
};
use crate::gateway::{
use crate::execution_runtime::{
resolve_local_sync_error_background_report_kind,
resolve_local_sync_success_background_report_kind, GatewayControlSyncDecisionResponse,
GatewaySyncReportRequest,
resolve_local_sync_success_background_report_kind,
};
use crate::gateway::{should_bypass_intent_decision, should_bypass_intent_plan};
use crate::intent::{should_bypass_intent_decision, should_bypass_intent_plan};
use crate::usage::GatewaySyncReportRequest;
fn test_parts() -> http::request::Parts {
let request = Request::builder()
@@ -247,7 +248,7 @@ fn resolve_local_sync_success_background_report_kind_maps_video_finalize_kinds()
for (report_kind, expected) in cases {
assert_eq!(
resolve_local_sync_success_background_report_kind(report_kind),
expected.map(str::to_string),
expected,
"unexpected mapping for {report_kind}"
);
}
@@ -286,7 +287,7 @@ fn resolve_local_sync_error_background_report_kind_maps_video_finalize_kinds() {
for (report_kind, expected) in cases {
assert_eq!(
resolve_local_sync_error_background_report_kind(report_kind),
expected.map(str::to_string),
expected,
"unexpected mapping for {report_kind}"
);
}
@@ -17,8 +17,8 @@ use serde_json::Value;
use thiserror::Error;
#[cfg(test)]
use crate::gateway::execution_runtime::remote_compat::execute_sync_plan_via_remote_execution_runtime;
use crate::gateway::{AppState, GatewayError};
use crate::execution_runtime::remote_compat::execute_sync_plan_via_remote_execution_runtime;
use crate::{AppState, GatewayError};
const HUB_RELAY_CONTENT_TYPE: &str = "application/vnd.aether.tunnel-envelope";
const HUB_RELAY_ERROR_HEADER: &str = "x-aether-tunnel-error";
@@ -138,6 +138,7 @@ impl DirectSyncExecutionRuntime {
let started_at = Instant::now();
let response = send_request(&plan, body_bytes).await?;
let ttfb_ms = started_at.elapsed().as_millis() as u64;
let status_code = response.status().as_u16();
let headers = collect_response_headers(response.headers());
let body_bytes = response.bytes().await.map_err(|err| {
@@ -174,7 +175,7 @@ impl DirectSyncExecutionRuntime {
headers,
body,
telemetry: Some(ExecutionTelemetry {
ttfb_ms: None,
ttfb_ms: Some(ttfb_ms),
elapsed_ms: Some(elapsed_ms),
upstream_bytes: Some(upstream_bytes),
}),
@@ -934,4 +935,68 @@ mod tests {
}))
);
}
#[tokio::test]
async fn direct_sync_execution_runtime_reports_ttfb_once_upstream_response_starts() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("listener should bind");
let addr = listener.local_addr().expect("local addr should resolve");
let app = Router::new().route(
"/chat",
post(|| async {
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
(axum::http::StatusCode::OK, Json(json!({"ok": true})))
}),
);
let server = tokio::spawn(async move {
axum::serve(listener, app)
.await
.expect("test server should run");
});
let execution_runtime = DirectSyncExecutionRuntime::new();
let result = execution_runtime
.execute_sync(ExecutionPlan {
request_id: "req-ttfb-1".into(),
candidate_id: Some("cand-1".into()),
provider_name: Some("openai".into()),
provider_id: "prov-1".into(),
endpoint_id: "ep-1".into(),
key_id: "key-1".into(),
method: "POST".into(),
url: format!("http://{addr}/chat"),
headers: BTreeMap::from([("content-type".into(), "application/json".into())]),
content_type: Some("application/json".into()),
content_encoding: None,
body: RequestBody::from_json(json!({"model": "gpt-4.1"})),
stream: false,
client_api_format: "openai:chat".into(),
provider_api_format: "openai:chat".into(),
model_name: Some("gpt-4.1".into()),
proxy: None,
tls_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(5_000),
total_ms: Some(5_000),
..ExecutionTimeouts::default()
}),
})
.await
.expect("sync execution should succeed");
server.abort();
let telemetry = result
.telemetry
.expect("sync execution should include telemetry");
let ttfb_ms = telemetry
.ttfb_ms
.expect("sync execution should include ttfb");
let elapsed_ms = telemetry
.elapsed_ms
.expect("sync execution should include elapsed time");
assert!(ttfb_ms > 0);
assert!(elapsed_ms >= ttfb_ms);
}
}