mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-12 14:10:19 +08:00
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:
@@ -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"
|
||||
);
|
||||
|
||||
+4
-3
@@ -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())
|
||||
}
|
||||
|
||||
+4
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user