mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
refactor: 移除独立 hub/proxy/executor/gateway crate,统一为 gateway tunnel 架构
- 删除 aether-hub、aether-proxy 独立项目及其 Dockerfile/配置 - 删除 crates/aether-executor 和 crates/aether-gateway 全部模块 - 新增 apps/ 目录作为应用入口 - 将 hub 概念重构为 gateway tunnel transport - 将 executor 重构为 execution runtime - 新增 tunnel.rs 合约定义和 testkit tunnel/execution_runtime 模块 - 更新 Python 服务层和测试适配新架构命名
This commit is contained in:
908
apps/aether-gateway/src/handlers/internal/gateway.rs
Normal file
908
apps/aether-gateway/src/handlers/internal/gateway.rs
Normal file
@@ -0,0 +1,908 @@
|
||||
use super::*;
|
||||
use crate::gateway::ai_pipeline::{planner as ai_planner, runtime as ai_runtime};
|
||||
|
||||
pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
remote_addr: &std::net::SocketAddr,
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
legacy_internal_gateway_allowed: bool,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
let Some(decision) = request_context.control_decision.as_ref() else {
|
||||
return Ok(None);
|
||||
};
|
||||
if decision.route_class.as_deref() != Some("internal_proxy") {
|
||||
return Ok(None);
|
||||
}
|
||||
if decision.route_family.as_deref() == Some("gateway_legacy") {
|
||||
if !legacy_internal_gateway_allowed {
|
||||
return Ok(Some(build_retired_internal_gateway_response()));
|
||||
}
|
||||
state.record_fallback_metric(
|
||||
GatewayFallbackMetricKind::LegacyInternalBridge,
|
||||
Some(decision),
|
||||
None,
|
||||
Some("legacy_internal_bridge"),
|
||||
GatewayFallbackReason::LegacyInternalGateway,
|
||||
);
|
||||
if !remote_addr.ip().is_loopback() {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::FORBIDDEN,
|
||||
"loopback access only",
|
||||
)));
|
||||
}
|
||||
match decision.route_kind.as_deref() {
|
||||
Some("resolve") if request_context.request_path == "/api/internal/gateway/resolve" => {
|
||||
let Some(request_body) = request_body else {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway resolve payload",
|
||||
)));
|
||||
};
|
||||
let payload =
|
||||
match serde_json::from_slice::<LegacyGatewayResolveRequest>(request_body) {
|
||||
Ok(payload) => payload,
|
||||
Err(_) => {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway resolve payload",
|
||||
)));
|
||||
}
|
||||
};
|
||||
let headers = match build_internal_gateway_header_map(&payload.headers) {
|
||||
Ok(headers) => headers,
|
||||
Err(response) => return Ok(Some(response)),
|
||||
};
|
||||
let method = match http::Method::from_bytes(payload.method.as_bytes()) {
|
||||
Ok(method) => method,
|
||||
Err(_) => {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway method",
|
||||
)));
|
||||
}
|
||||
};
|
||||
let uri = match build_internal_gateway_uri(
|
||||
&payload.path,
|
||||
payload.query_string.as_deref(),
|
||||
) {
|
||||
Ok(uri) => uri,
|
||||
Err(response) => return Ok(Some(response)),
|
||||
};
|
||||
let resolved = crate::gateway::control::resolve_control_route(
|
||||
state,
|
||||
&method,
|
||||
&uri,
|
||||
&headers,
|
||||
payload
|
||||
.trace_id
|
||||
.as_deref()
|
||||
.unwrap_or(request_context.trace_id.as_str()),
|
||||
)
|
||||
.await?;
|
||||
let response_payload = resolved
|
||||
.map(build_internal_gateway_resolve_payload)
|
||||
.unwrap_or_else(|| build_internal_gateway_passthrough_payload(&uri));
|
||||
return Ok(Some(Json(response_payload).into_response()));
|
||||
}
|
||||
Some("auth_context")
|
||||
if request_context.request_path == "/api/internal/gateway/auth-context" =>
|
||||
{
|
||||
let Some(request_body) = request_body else {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway auth-context payload",
|
||||
)));
|
||||
};
|
||||
let payload =
|
||||
match serde_json::from_slice::<LegacyGatewayAuthContextRequest>(request_body) {
|
||||
Ok(payload) => payload,
|
||||
Err(_) => {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway auth-context payload",
|
||||
)));
|
||||
}
|
||||
};
|
||||
let headers = match build_internal_gateway_header_map(&payload.headers) {
|
||||
Ok(headers) => headers,
|
||||
Err(response) => return Ok(Some(response)),
|
||||
};
|
||||
let uri = match build_internal_gateway_uri("/", payload.query_string.as_deref()) {
|
||||
Ok(uri) => uri,
|
||||
Err(response) => return Ok(Some(response)),
|
||||
};
|
||||
let mut synthetic_decision = GatewayControlDecision::synthetic(
|
||||
"/",
|
||||
Some("internal_proxy".to_string()),
|
||||
Some("gateway_legacy".to_string()),
|
||||
Some("auth_context".to_string()),
|
||||
Some(payload.auth_endpoint_signature),
|
||||
);
|
||||
synthetic_decision.public_query_string = uri.query().map(ToOwned::to_owned);
|
||||
let auth_context = crate::gateway::resolve_execution_runtime_auth_context(
|
||||
state,
|
||||
&synthetic_decision,
|
||||
&headers,
|
||||
&uri,
|
||||
payload
|
||||
.trace_id
|
||||
.as_deref()
|
||||
.unwrap_or(request_context.trace_id.as_str()),
|
||||
)
|
||||
.await?;
|
||||
return Ok(Some(
|
||||
Json(json!({ "auth_context": auth_context })).into_response(),
|
||||
));
|
||||
}
|
||||
Some("decision_sync")
|
||||
if request_context.request_path == "/api/internal/gateway/decision-sync" =>
|
||||
{
|
||||
let Some(request_body) = request_body else {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway decision-sync payload",
|
||||
)));
|
||||
};
|
||||
let payload =
|
||||
match serde_json::from_slice::<LegacyGatewayExecuteRequest>(request_body) {
|
||||
Ok(payload) => payload,
|
||||
Err(_) => {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway decision-sync payload",
|
||||
)));
|
||||
}
|
||||
};
|
||||
let parts = match build_internal_gateway_request_parts(
|
||||
&payload.method,
|
||||
&payload.path,
|
||||
payload.query_string.as_deref(),
|
||||
&payload.headers,
|
||||
) {
|
||||
Ok(parts) => parts,
|
||||
Err(response) => return Ok(Some(response)),
|
||||
};
|
||||
let trace_id = payload
|
||||
.trace_id
|
||||
.as_deref()
|
||||
.unwrap_or(request_context.trace_id.as_str())
|
||||
.to_string();
|
||||
let body_is_empty = payload.body_base64.is_none()
|
||||
&& payload
|
||||
.body_json
|
||||
.as_object()
|
||||
.map(|value| value.is_empty())
|
||||
.unwrap_or(false);
|
||||
let Some(mut resolved) = crate::gateway::resolve_control_route(
|
||||
state,
|
||||
&parts.method,
|
||||
&parts.uri,
|
||||
&parts.headers,
|
||||
trace_id.as_str(),
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
return Ok(Some(
|
||||
Json(build_internal_gateway_fallback_plan_payload(None)).into_response(),
|
||||
));
|
||||
};
|
||||
if let Some(auth_context) = payload.auth_context.clone() {
|
||||
resolved.auth_context = Some(auth_context);
|
||||
resolved.local_auth_rejection = None;
|
||||
}
|
||||
let auth_context = resolved.auth_context.clone();
|
||||
if auth_context
|
||||
.as_ref()
|
||||
.map(|value| !value.access_allowed)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
let fallback_auth_context = if payload.auth_context.is_none() {
|
||||
auth_context.as_ref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
return Ok(Some(
|
||||
Json(build_internal_gateway_fallback_plan_payload(
|
||||
fallback_auth_context,
|
||||
))
|
||||
.into_response(),
|
||||
));
|
||||
}
|
||||
let Some(mut local_payload) = ai_planner::maybe_build_sync_decision_payload(
|
||||
state,
|
||||
&parts,
|
||||
trace_id.as_str(),
|
||||
&resolved,
|
||||
&payload.body_json,
|
||||
payload.body_base64.as_deref(),
|
||||
body_is_empty,
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
let fallback_auth_context = if payload.auth_context.is_none() {
|
||||
auth_context.as_ref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
return Ok(Some(
|
||||
Json(build_internal_gateway_fallback_plan_payload(
|
||||
fallback_auth_context,
|
||||
))
|
||||
.into_response(),
|
||||
));
|
||||
};
|
||||
if payload.auth_context.is_some() {
|
||||
local_payload.auth_context = None;
|
||||
}
|
||||
return Ok(Some(Json(local_payload).into_response()));
|
||||
}
|
||||
Some("decision_stream")
|
||||
if request_context.request_path == "/api/internal/gateway/decision-stream" =>
|
||||
{
|
||||
let Some(request_body) = request_body else {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway decision-stream payload",
|
||||
)));
|
||||
};
|
||||
let payload =
|
||||
match serde_json::from_slice::<LegacyGatewayExecuteRequest>(request_body) {
|
||||
Ok(payload) => payload,
|
||||
Err(_) => {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway decision-stream payload",
|
||||
)));
|
||||
}
|
||||
};
|
||||
let parts = match build_internal_gateway_request_parts(
|
||||
&payload.method,
|
||||
&payload.path,
|
||||
payload.query_string.as_deref(),
|
||||
&payload.headers,
|
||||
) {
|
||||
Ok(parts) => parts,
|
||||
Err(response) => return Ok(Some(response)),
|
||||
};
|
||||
let trace_id = payload
|
||||
.trace_id
|
||||
.as_deref()
|
||||
.unwrap_or(request_context.trace_id.as_str())
|
||||
.to_string();
|
||||
let Some(mut resolved) = crate::gateway::resolve_control_route(
|
||||
state,
|
||||
&parts.method,
|
||||
&parts.uri,
|
||||
&parts.headers,
|
||||
trace_id.as_str(),
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
return Ok(Some(
|
||||
Json(build_internal_gateway_fallback_plan_payload(None)).into_response(),
|
||||
));
|
||||
};
|
||||
if let Some(auth_context) = payload.auth_context.clone() {
|
||||
resolved.auth_context = Some(auth_context);
|
||||
resolved.local_auth_rejection = None;
|
||||
}
|
||||
let auth_context = resolved.auth_context.clone();
|
||||
if auth_context
|
||||
.as_ref()
|
||||
.map(|value| !value.access_allowed)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
let fallback_auth_context = if payload.auth_context.is_none() {
|
||||
auth_context.as_ref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
return Ok(Some(
|
||||
Json(build_internal_gateway_fallback_plan_payload(
|
||||
fallback_auth_context,
|
||||
))
|
||||
.into_response(),
|
||||
));
|
||||
}
|
||||
let Some(mut local_payload) = ai_planner::maybe_build_stream_decision_payload(
|
||||
state,
|
||||
&parts,
|
||||
trace_id.as_str(),
|
||||
&resolved,
|
||||
&payload.body_json,
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
let fallback_auth_context = if payload.auth_context.is_none() {
|
||||
auth_context.as_ref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
return Ok(Some(
|
||||
Json(build_internal_gateway_fallback_plan_payload(
|
||||
fallback_auth_context,
|
||||
))
|
||||
.into_response(),
|
||||
));
|
||||
};
|
||||
if payload.auth_context.is_some() {
|
||||
local_payload.auth_context = None;
|
||||
}
|
||||
return Ok(Some(Json(local_payload).into_response()));
|
||||
}
|
||||
Some("plan_sync")
|
||||
if request_context.request_path == "/api/internal/gateway/plan-sync" =>
|
||||
{
|
||||
let Some(request_body) = request_body else {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway plan-sync payload",
|
||||
)));
|
||||
};
|
||||
let payload =
|
||||
match serde_json::from_slice::<LegacyGatewayExecuteRequest>(request_body) {
|
||||
Ok(payload) => payload,
|
||||
Err(_) => {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway plan-sync payload",
|
||||
)));
|
||||
}
|
||||
};
|
||||
let parts = match build_internal_gateway_request_parts(
|
||||
&payload.method,
|
||||
&payload.path,
|
||||
payload.query_string.as_deref(),
|
||||
&payload.headers,
|
||||
) {
|
||||
Ok(parts) => parts,
|
||||
Err(response) => return Ok(Some(response)),
|
||||
};
|
||||
let trace_id = payload
|
||||
.trace_id
|
||||
.as_deref()
|
||||
.unwrap_or(request_context.trace_id.as_str())
|
||||
.to_string();
|
||||
let body_is_empty = payload.body_base64.is_none()
|
||||
&& payload
|
||||
.body_json
|
||||
.as_object()
|
||||
.map(|value| value.is_empty())
|
||||
.unwrap_or(false);
|
||||
let body_bytes = match decode_legacy_gateway_request_body_bytes(&payload) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(response) => return Ok(Some(response)),
|
||||
};
|
||||
let Some(mut resolved) = crate::gateway::resolve_control_route(
|
||||
state,
|
||||
&parts.method,
|
||||
&parts.uri,
|
||||
&parts.headers,
|
||||
trace_id.as_str(),
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
return Ok(Some(build_internal_gateway_proxy_public_response()));
|
||||
};
|
||||
if let Some(auth_context) = payload.auth_context.clone() {
|
||||
resolved.auth_context = Some(auth_context);
|
||||
resolved.local_auth_rejection = None;
|
||||
}
|
||||
if let Some(mut planned) = maybe_build_sync_plan_payload(
|
||||
state,
|
||||
&parts,
|
||||
trace_id.as_str(),
|
||||
&resolved,
|
||||
&payload.body_json,
|
||||
payload.body_base64.as_deref(),
|
||||
body_is_empty,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if payload.auth_context.is_some() {
|
||||
planned.auth_context = None;
|
||||
}
|
||||
return Ok(Some(Json(planned).into_response()));
|
||||
}
|
||||
if let Some(executed_response) = ai_runtime::maybe_execute_sync_request(
|
||||
state,
|
||||
&parts,
|
||||
&body_bytes,
|
||||
trace_id.as_str(),
|
||||
Some(&resolved),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(attach_execution_path_header(
|
||||
executed_response,
|
||||
EXECUTION_PATH_EXECUTION_RUNTIME_SYNC,
|
||||
)));
|
||||
}
|
||||
return Ok(Some(build_internal_gateway_proxy_public_response()));
|
||||
}
|
||||
Some("plan_stream")
|
||||
if request_context.request_path == "/api/internal/gateway/plan-stream" =>
|
||||
{
|
||||
let Some(request_body) = request_body else {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway plan-stream payload",
|
||||
)));
|
||||
};
|
||||
let payload =
|
||||
match serde_json::from_slice::<LegacyGatewayExecuteRequest>(request_body) {
|
||||
Ok(payload) => payload,
|
||||
Err(_) => {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway plan-stream payload",
|
||||
)));
|
||||
}
|
||||
};
|
||||
let parts = match build_internal_gateway_request_parts(
|
||||
&payload.method,
|
||||
&payload.path,
|
||||
payload.query_string.as_deref(),
|
||||
&payload.headers,
|
||||
) {
|
||||
Ok(parts) => parts,
|
||||
Err(response) => return Ok(Some(response)),
|
||||
};
|
||||
let trace_id = payload
|
||||
.trace_id
|
||||
.as_deref()
|
||||
.unwrap_or(request_context.trace_id.as_str())
|
||||
.to_string();
|
||||
let body_bytes = match decode_legacy_gateway_request_body_bytes(&payload) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(response) => return Ok(Some(response)),
|
||||
};
|
||||
let Some(mut resolved) = crate::gateway::resolve_control_route(
|
||||
state,
|
||||
&parts.method,
|
||||
&parts.uri,
|
||||
&parts.headers,
|
||||
trace_id.as_str(),
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
return Ok(Some(build_internal_gateway_proxy_public_response()));
|
||||
};
|
||||
if let Some(auth_context) = payload.auth_context.clone() {
|
||||
resolved.auth_context = Some(auth_context);
|
||||
resolved.local_auth_rejection = None;
|
||||
}
|
||||
if let Some(mut planned) = maybe_build_stream_plan_payload(
|
||||
state,
|
||||
&parts,
|
||||
trace_id.as_str(),
|
||||
&resolved,
|
||||
&payload.body_json,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if payload.auth_context.is_some() {
|
||||
planned.auth_context = None;
|
||||
}
|
||||
return Ok(Some(Json(planned).into_response()));
|
||||
}
|
||||
if let Some(executed_response) = ai_runtime::maybe_execute_stream_request(
|
||||
state,
|
||||
&parts,
|
||||
&body_bytes,
|
||||
trace_id.as_str(),
|
||||
Some(&resolved),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(attach_execution_path_header(
|
||||
executed_response,
|
||||
EXECUTION_PATH_EXECUTION_RUNTIME_STREAM,
|
||||
)));
|
||||
}
|
||||
return Ok(Some(build_internal_gateway_proxy_public_response()));
|
||||
}
|
||||
Some("execute_sync")
|
||||
if request_context.request_path == "/api/internal/gateway/execute-sync" =>
|
||||
{
|
||||
let Some(request_body) = request_body else {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway execute-sync payload",
|
||||
)));
|
||||
};
|
||||
let payload =
|
||||
match serde_json::from_slice::<LegacyGatewayExecuteRequest>(request_body) {
|
||||
Ok(payload) => payload,
|
||||
Err(_) => {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway execute-sync payload",
|
||||
)));
|
||||
}
|
||||
};
|
||||
let parts = match build_internal_gateway_request_parts(
|
||||
&payload.method,
|
||||
&payload.path,
|
||||
payload.query_string.as_deref(),
|
||||
&payload.headers,
|
||||
) {
|
||||
Ok(parts) => parts,
|
||||
Err(response) => return Ok(Some(response)),
|
||||
};
|
||||
let trace_id = payload
|
||||
.trace_id
|
||||
.as_deref()
|
||||
.unwrap_or(request_context.trace_id.as_str())
|
||||
.to_string();
|
||||
let body_bytes = match decode_legacy_gateway_request_body_bytes(&payload) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(response) => return Ok(Some(response)),
|
||||
};
|
||||
let Some(mut resolved) = crate::gateway::resolve_control_route(
|
||||
state,
|
||||
&parts.method,
|
||||
&parts.uri,
|
||||
&parts.headers,
|
||||
trace_id.as_str(),
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
if let Some(auth_context) = payload.auth_context.clone() {
|
||||
resolved.auth_context = Some(auth_context);
|
||||
resolved.local_auth_rejection = None;
|
||||
}
|
||||
if let Some(plan_payload) = maybe_build_sync_plan_payload(
|
||||
state,
|
||||
&parts,
|
||||
trace_id.as_str(),
|
||||
&resolved,
|
||||
&payload.body_json,
|
||||
payload.body_base64.as_deref(),
|
||||
body_bytes.is_empty(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
let plan_kind = plan_payload.plan_kind.unwrap_or_default();
|
||||
if let Some(plan) = plan_payload.plan {
|
||||
if !plan_kind.trim().is_empty() {
|
||||
let executed_response = execute_execution_runtime_sync(
|
||||
state,
|
||||
parts.uri.path(),
|
||||
plan,
|
||||
trace_id.as_str(),
|
||||
&resolved,
|
||||
plan_kind.as_str(),
|
||||
plan_payload.report_kind,
|
||||
plan_payload.report_context,
|
||||
)
|
||||
.await?;
|
||||
if let Some(executed_response) = executed_response {
|
||||
return Ok(Some(attach_execution_path_header(
|
||||
executed_response,
|
||||
EXECUTION_PATH_EXECUTION_RUNTIME_SYNC,
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(executed_response) = ai_runtime::maybe_execute_sync_request(
|
||||
state,
|
||||
&parts,
|
||||
&body_bytes,
|
||||
trace_id.as_str(),
|
||||
Some(&resolved),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(attach_execution_path_header(
|
||||
executed_response,
|
||||
EXECUTION_PATH_EXECUTION_RUNTIME_SYNC,
|
||||
)));
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
Some("execute_stream")
|
||||
if request_context.request_path == "/api/internal/gateway/execute-stream" =>
|
||||
{
|
||||
let Some(request_body) = request_body else {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway execute-stream payload",
|
||||
)));
|
||||
};
|
||||
let payload =
|
||||
match serde_json::from_slice::<LegacyGatewayExecuteRequest>(request_body) {
|
||||
Ok(payload) => payload,
|
||||
Err(_) => {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway execute-stream payload",
|
||||
)));
|
||||
}
|
||||
};
|
||||
let parts = match build_internal_gateway_request_parts(
|
||||
&payload.method,
|
||||
&payload.path,
|
||||
payload.query_string.as_deref(),
|
||||
&payload.headers,
|
||||
) {
|
||||
Ok(parts) => parts,
|
||||
Err(response) => return Ok(Some(response)),
|
||||
};
|
||||
let trace_id = payload
|
||||
.trace_id
|
||||
.as_deref()
|
||||
.unwrap_or(request_context.trace_id.as_str())
|
||||
.to_string();
|
||||
let body_bytes = match decode_legacy_gateway_request_body_bytes(&payload) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(response) => return Ok(Some(response)),
|
||||
};
|
||||
let Some(mut resolved) = crate::gateway::resolve_control_route(
|
||||
state,
|
||||
&parts.method,
|
||||
&parts.uri,
|
||||
&parts.headers,
|
||||
trace_id.as_str(),
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
if let Some(auth_context) = payload.auth_context.clone() {
|
||||
resolved.auth_context = Some(auth_context);
|
||||
resolved.local_auth_rejection = None;
|
||||
}
|
||||
if let Some(plan_payload) = maybe_build_stream_plan_payload(
|
||||
state,
|
||||
&parts,
|
||||
trace_id.as_str(),
|
||||
&resolved,
|
||||
&payload.body_json,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
let plan_kind = plan_payload.plan_kind.unwrap_or_default();
|
||||
if let Some(plan) = plan_payload.plan {
|
||||
if !plan_kind.trim().is_empty() {
|
||||
let executed_response = execute_execution_runtime_stream(
|
||||
state,
|
||||
plan,
|
||||
trace_id.as_str(),
|
||||
&resolved,
|
||||
plan_kind.as_str(),
|
||||
plan_payload.report_kind,
|
||||
plan_payload.report_context,
|
||||
)
|
||||
.await?;
|
||||
if let Some(executed_response) = executed_response {
|
||||
return Ok(Some(attach_execution_path_header(
|
||||
executed_response,
|
||||
EXECUTION_PATH_EXECUTION_RUNTIME_STREAM,
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(executed_response) = ai_runtime::maybe_execute_stream_request(
|
||||
state,
|
||||
&parts,
|
||||
&body_bytes,
|
||||
trace_id.as_str(),
|
||||
Some(&resolved),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(attach_execution_path_header(
|
||||
executed_response,
|
||||
EXECUTION_PATH_EXECUTION_RUNTIME_STREAM,
|
||||
)));
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
Some("report_sync")
|
||||
if request_context.request_path == "/api/internal/gateway/report-sync" =>
|
||||
{
|
||||
let Some(request_body) = request_body else {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway report-sync payload",
|
||||
)));
|
||||
};
|
||||
let payload = match serde_json::from_slice::<crate::gateway::GatewaySyncReportRequest>(
|
||||
request_body,
|
||||
) {
|
||||
Ok(payload) => payload,
|
||||
Err(_) => {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway report-sync payload",
|
||||
)));
|
||||
}
|
||||
};
|
||||
let trace_id = payload.trace_id.clone();
|
||||
crate::gateway::usage::submit_sync_report(state, &trace_id, payload).await?;
|
||||
return Ok(Some(Json(json!({ "ok": true })).into_response()));
|
||||
}
|
||||
Some("report_stream")
|
||||
if request_context.request_path == "/api/internal/gateway/report-stream" =>
|
||||
{
|
||||
let Some(request_body) = request_body else {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway report-stream payload",
|
||||
)));
|
||||
};
|
||||
let payload = match serde_json::from_slice::<
|
||||
crate::gateway::GatewayStreamReportRequest,
|
||||
>(request_body)
|
||||
{
|
||||
Ok(payload) => payload,
|
||||
Err(_) => {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway report-stream payload",
|
||||
)));
|
||||
}
|
||||
};
|
||||
let trace_id = payload.trace_id.clone();
|
||||
crate::gateway::usage::submit_stream_report(state, &trace_id, payload).await?;
|
||||
return Ok(Some(Json(json!({ "ok": true })).into_response()));
|
||||
}
|
||||
Some("finalize_sync")
|
||||
if request_context.request_path == "/api/internal/gateway/finalize-sync" =>
|
||||
{
|
||||
let Some(request_body) = request_body else {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway finalize-sync payload",
|
||||
)));
|
||||
};
|
||||
let payload = match serde_json::from_slice::<crate::gateway::GatewaySyncReportRequest>(
|
||||
request_body,
|
||||
) {
|
||||
Ok(payload) => payload,
|
||||
Err(_) => {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway finalize-sync payload",
|
||||
)));
|
||||
}
|
||||
};
|
||||
let Some(synthetic_decision) = build_legacy_finalize_decision(&payload) else {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"Unsupported gateway sync finalize kind",
|
||||
)));
|
||||
};
|
||||
let trace_id = payload.trace_id.clone();
|
||||
if let Some(outcome) =
|
||||
maybe_build_sync_finalize_outcome(&trace_id, &synthetic_decision, &payload)?
|
||||
{
|
||||
if let Some(background_report) = outcome.background_report {
|
||||
crate::gateway::usage::spawn_sync_report(
|
||||
state.clone(),
|
||||
trace_id.clone(),
|
||||
background_report,
|
||||
);
|
||||
}
|
||||
let mut response = outcome.response;
|
||||
response.headers_mut().insert(
|
||||
HeaderName::from_static(CONTROL_EXECUTED_HEADER),
|
||||
HeaderValue::from_static("true"),
|
||||
);
|
||||
return Ok(Some(response));
|
||||
}
|
||||
if let Some(response) = maybe_build_legacy_finalize_video_response(
|
||||
state,
|
||||
trace_id.as_str(),
|
||||
&synthetic_decision,
|
||||
&payload,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(Some(response));
|
||||
}
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"Unsupported gateway sync finalize kind",
|
||||
)));
|
||||
}
|
||||
_ => return Ok(None),
|
||||
}
|
||||
}
|
||||
if !remote_addr.ip().is_loopback() {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::FORBIDDEN,
|
||||
"loopback access only",
|
||||
)));
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() != Some(crate::gateway::TUNNEL_ROUTE_FAMILY)
|
||||
|| request_context.request_method != http::Method::POST
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
match decision.route_kind.as_deref() {
|
||||
Some("heartbeat")
|
||||
if crate::gateway::is_tunnel_heartbeat_path(request_context.request_path.as_str()) =>
|
||||
{
|
||||
let Some(request_body) = request_body else {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid heartbeat payload",
|
||||
)));
|
||||
};
|
||||
let payload = match parse_internal_tunnel_heartbeat_request(request_body) {
|
||||
Ok(payload) => payload,
|
||||
Err(response) => return Ok(Some(response)),
|
||||
};
|
||||
let node_id = payload.node_id.trim().to_string();
|
||||
let mutation = ProxyNodeHeartbeatMutation {
|
||||
node_id: node_id.clone(),
|
||||
heartbeat_interval: payload.heartbeat_interval,
|
||||
active_connections: payload.active_connections,
|
||||
total_requests_delta: payload.total_requests,
|
||||
avg_latency_ms: payload.avg_latency_ms,
|
||||
failed_requests_delta: payload.failed_requests,
|
||||
dns_failures_delta: payload.dns_failures,
|
||||
stream_errors_delta: payload.stream_errors,
|
||||
proxy_metadata: payload.proxy_metadata,
|
||||
proxy_version: payload.proxy_version,
|
||||
};
|
||||
|
||||
let response = match state.apply_proxy_node_heartbeat(&mutation).await {
|
||||
Ok(Some(node)) => Json(build_internal_tunnel_heartbeat_ack(&node)).into_response(),
|
||||
Ok(None) => build_internal_control_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("heartbeat sync failed: ProxyNode {node_id} 不存在"),
|
||||
),
|
||||
Err(err) => build_internal_control_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("heartbeat sync failed: {}", gateway_error_message(err)),
|
||||
),
|
||||
};
|
||||
return Ok(Some(response));
|
||||
}
|
||||
Some("node_status")
|
||||
if crate::gateway::is_tunnel_node_status_path(
|
||||
request_context.request_path.as_str(),
|
||||
) =>
|
||||
{
|
||||
let Some(request_body) = request_body else {
|
||||
return Ok(Some(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid node-status payload",
|
||||
)));
|
||||
};
|
||||
let payload = match parse_internal_tunnel_node_status_request(request_body) {
|
||||
Ok(payload) => payload,
|
||||
Err(response) => return Ok(Some(response)),
|
||||
};
|
||||
let mutation = ProxyNodeTunnelStatusMutation {
|
||||
node_id: payload.node_id.trim().to_string(),
|
||||
connected: payload.connected,
|
||||
conn_count: payload.conn_count,
|
||||
detail: None,
|
||||
observed_at_unix_secs: None,
|
||||
};
|
||||
|
||||
let response = match state.update_proxy_node_tunnel_status(&mutation).await {
|
||||
Ok(node) => Json(json!({ "updated": node.is_some() })).into_response(),
|
||||
Err(err) => build_internal_control_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("node status sync failed: {}", gateway_error_message(err)),
|
||||
),
|
||||
};
|
||||
return Ok(Some(response));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
709
apps/aether-gateway/src/handlers/internal/gateway_helpers.rs
Normal file
709
apps/aether-gateway/src/handlers/internal/gateway_helpers.rs
Normal file
@@ -0,0 +1,709 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) fn build_retired_internal_gateway_response() -> Response<Body> {
|
||||
attach_legacy_internal_gateway_deprecation_headers(
|
||||
(
|
||||
http::StatusCode::GONE,
|
||||
Json(json!({
|
||||
"detail": "legacy internal gateway route removed; use public proxy",
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
)
|
||||
}
|
||||
|
||||
fn insert_execution_runtime_candidate_fields(
|
||||
payload: &mut serde_json::Map<String, serde_json::Value>,
|
||||
value: bool,
|
||||
) {
|
||||
payload.insert(
|
||||
CONTROL_LEGACY_EXECUTION_RUNTIME_CANDIDATE_KEY.to_string(),
|
||||
json!(value),
|
||||
);
|
||||
payload.insert(
|
||||
CONTROL_EXECUTION_RUNTIME_CANDIDATE_KEY.to_string(),
|
||||
json!(value),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn build_internal_gateway_passthrough_payload(uri: &http::Uri) -> serde_json::Value {
|
||||
let mut payload = serde_json::Map::new();
|
||||
payload.insert("action".to_string(), json!("proxy_public"));
|
||||
payload.insert("route_class".to_string(), json!("passthrough"));
|
||||
payload.insert("public_path".to_string(), json!(uri.path()));
|
||||
insert_execution_runtime_candidate_fields(&mut payload, false);
|
||||
if let Some(query) = uri.query().filter(|value| !value.is_empty()) {
|
||||
payload.insert("public_query_string".to_string(), json!(query));
|
||||
}
|
||||
serde_json::Value::Object(payload)
|
||||
}
|
||||
|
||||
pub(crate) fn build_internal_gateway_resolve_payload(
|
||||
decision: GatewayControlDecision,
|
||||
) -> serde_json::Value {
|
||||
let mut payload = serde_json::Map::new();
|
||||
payload.insert("action".to_string(), json!("proxy_public"));
|
||||
payload.insert("route_class".to_string(), json!(decision.route_class));
|
||||
payload.insert("public_path".to_string(), json!(decision.public_path));
|
||||
insert_execution_runtime_candidate_fields(
|
||||
&mut payload,
|
||||
decision.is_execution_runtime_candidate(),
|
||||
);
|
||||
if let Some(query) = decision.public_query_string {
|
||||
payload.insert("public_query_string".to_string(), json!(query));
|
||||
}
|
||||
if let Some(route_family) = decision.route_family {
|
||||
payload.insert("route_family".to_string(), json!(route_family));
|
||||
}
|
||||
if let Some(route_kind) = decision.route_kind {
|
||||
payload.insert("route_kind".to_string(), json!(route_kind));
|
||||
}
|
||||
if let Some(signature) = decision.auth_endpoint_signature {
|
||||
payload.insert("auth_endpoint_signature".to_string(), json!(signature));
|
||||
}
|
||||
if let Some(auth_context) = decision.auth_context {
|
||||
payload.insert(
|
||||
"auth_context".to_string(),
|
||||
serde_json::to_value(auth_context).unwrap_or(serde_json::Value::Null),
|
||||
);
|
||||
}
|
||||
serde_json::Value::Object(payload)
|
||||
}
|
||||
|
||||
pub(crate) fn build_internal_gateway_fallback_plan_payload(
|
||||
auth_context: Option<&crate::gateway::GatewayControlAuthContext>,
|
||||
) -> serde_json::Value {
|
||||
let mut payload = serde_json::Map::new();
|
||||
payload.insert("action".to_string(), json!("fallback_plan"));
|
||||
if let Some(auth_context) = auth_context {
|
||||
payload.insert(
|
||||
"auth_context".to_string(),
|
||||
serde_json::to_value(auth_context).unwrap_or(serde_json::Value::Null),
|
||||
);
|
||||
}
|
||||
serde_json::Value::Object(payload)
|
||||
}
|
||||
|
||||
pub(crate) fn build_internal_gateway_proxy_public_response() -> Response<Body> {
|
||||
attach_legacy_internal_gateway_deprecation_headers(
|
||||
(
|
||||
http::StatusCode::CONFLICT,
|
||||
[(CONTROL_ACTION_HEADER, CONTROL_ACTION_PROXY_PUBLIC)],
|
||||
Json(json!({ "action": CONTROL_ACTION_PROXY_PUBLIC })),
|
||||
)
|
||||
.into_response(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn attach_legacy_internal_gateway_deprecation_headers(
|
||||
mut response: Response<Body>,
|
||||
) -> Response<Body> {
|
||||
response.headers_mut().insert(
|
||||
HeaderName::from_static(LEGACY_INTERNAL_GATEWAY_PHASEOUT_HEADER),
|
||||
HeaderValue::from_static(LEGACY_INTERNAL_GATEWAY_PHASEOUT_STATUS),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
HeaderName::from_static(LEGACY_INTERNAL_GATEWAY_SUNSET_DATE_HEADER),
|
||||
HeaderValue::from_static(LEGACY_INTERNAL_GATEWAY_SUNSET_DATE),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
HeaderName::from_static("sunset"),
|
||||
HeaderValue::from_static(LEGACY_INTERNAL_GATEWAY_SUNSET_HTTP_DATE),
|
||||
);
|
||||
response
|
||||
}
|
||||
|
||||
pub(crate) fn attach_execution_path_header(
|
||||
mut response: Response<Body>,
|
||||
execution_path: &'static str,
|
||||
) -> Response<Body> {
|
||||
response.headers_mut().insert(
|
||||
HeaderName::from_static(EXECUTION_PATH_HEADER),
|
||||
HeaderValue::from_static(execution_path),
|
||||
);
|
||||
response
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_local_proxy_execution_path(
|
||||
response: &Response<Body>,
|
||||
default_execution_path: &'static str,
|
||||
) -> &'static str {
|
||||
match response
|
||||
.headers()
|
||||
.get(EXECUTION_PATH_HEADER)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
{
|
||||
Some(EXECUTION_PATH_EXECUTION_RUNTIME_SYNC) => EXECUTION_PATH_EXECUTION_RUNTIME_SYNC,
|
||||
Some(EXECUTION_PATH_EXECUTION_RUNTIME_STREAM) => EXECUTION_PATH_EXECUTION_RUNTIME_STREAM,
|
||||
Some(EXECUTION_PATH_CONTROL_EXECUTE_SYNC) => EXECUTION_PATH_CONTROL_EXECUTE_SYNC,
|
||||
Some(EXECUTION_PATH_CONTROL_EXECUTE_STREAM) => EXECUTION_PATH_CONTROL_EXECUTE_STREAM,
|
||||
Some(EXECUTION_PATH_LOCAL_AUTH_DENIED) => EXECUTION_PATH_LOCAL_AUTH_DENIED,
|
||||
Some(EXECUTION_PATH_LOCAL_RATE_LIMITED) => EXECUTION_PATH_LOCAL_RATE_LIMITED,
|
||||
Some(EXECUTION_PATH_LOCAL_OVERLOADED) => EXECUTION_PATH_LOCAL_OVERLOADED,
|
||||
Some(EXECUTION_PATH_DISTRIBUTED_OVERLOADED) => EXECUTION_PATH_DISTRIBUTED_OVERLOADED,
|
||||
Some(EXECUTION_PATH_PUBLIC_PROXY_AFTER_EXECUTION_RUNTIME_MISS)
|
||||
| Some(LEGACY_EXECUTION_PATH_PUBLIC_PROXY_AFTER_EXECUTION_RUNTIME_MISS) => {
|
||||
EXECUTION_PATH_PUBLIC_PROXY_AFTER_EXECUTION_RUNTIME_MISS
|
||||
}
|
||||
Some(EXECUTION_PATH_PUBLIC_PROXY_PASSTHROUGH) => EXECUTION_PATH_PUBLIC_PROXY_PASSTHROUGH,
|
||||
_ => default_execution_path,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn decode_legacy_gateway_request_body_bytes(
|
||||
payload: &LegacyGatewayExecuteRequest,
|
||||
) -> Result<Bytes, Response<Body>> {
|
||||
if let Some(body_base64) = payload.body_base64.as_deref() {
|
||||
let decoded = base64::engine::general_purpose::STANDARD
|
||||
.decode(body_base64)
|
||||
.map_err(|_| {
|
||||
build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway body payload",
|
||||
)
|
||||
})?;
|
||||
return Ok(Bytes::from(decoded));
|
||||
}
|
||||
|
||||
let body_json = payload.body_json.clone();
|
||||
if body_json.is_null() {
|
||||
return Ok(Bytes::new());
|
||||
}
|
||||
if body_json
|
||||
.as_object()
|
||||
.map(|value| value.is_empty())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Ok(Bytes::new());
|
||||
}
|
||||
|
||||
let encoded = serde_json::to_vec(&body_json).map_err(|_| {
|
||||
build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway body payload",
|
||||
)
|
||||
})?;
|
||||
Ok(Bytes::from(encoded))
|
||||
}
|
||||
|
||||
pub(crate) fn build_internal_gateway_header_map(
|
||||
headers: &BTreeMap<String, String>,
|
||||
) -> Result<http::HeaderMap, Response<Body>> {
|
||||
let mut mapped = http::HeaderMap::new();
|
||||
for (name, value) in headers {
|
||||
let header_name = match HeaderName::from_bytes(name.as_bytes()) {
|
||||
Ok(name) => name,
|
||||
Err(_) => {
|
||||
return Err(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway header",
|
||||
));
|
||||
}
|
||||
};
|
||||
let header_value = match HeaderValue::from_str(value) {
|
||||
Ok(value) => value,
|
||||
Err(_) => {
|
||||
return Err(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway header",
|
||||
));
|
||||
}
|
||||
};
|
||||
mapped.append(header_name, header_value);
|
||||
}
|
||||
Ok(mapped)
|
||||
}
|
||||
|
||||
pub(crate) fn build_internal_gateway_request_parts(
|
||||
method: &str,
|
||||
path: &str,
|
||||
query_string: Option<&str>,
|
||||
headers: &BTreeMap<String, String>,
|
||||
) -> Result<http::request::Parts, Response<Body>> {
|
||||
let mapped_headers = build_internal_gateway_header_map(headers)?;
|
||||
let method = match http::Method::from_bytes(method.as_bytes()) {
|
||||
Ok(method) => method,
|
||||
Err(_) => {
|
||||
return Err(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway method",
|
||||
));
|
||||
}
|
||||
};
|
||||
let uri = build_internal_gateway_uri(path, query_string)?;
|
||||
let request = match http::Request::builder().method(method).uri(uri).body(()) {
|
||||
Ok(request) => request,
|
||||
Err(_) => {
|
||||
return Err(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway request",
|
||||
));
|
||||
}
|
||||
};
|
||||
let (mut parts, _) = request.into_parts();
|
||||
parts.headers = mapped_headers;
|
||||
Ok(parts)
|
||||
}
|
||||
|
||||
pub(crate) fn build_internal_gateway_uri(
|
||||
path: &str,
|
||||
query_string: Option<&str>,
|
||||
) -> Result<http::Uri, Response<Body>> {
|
||||
let normalized_path = if path.starts_with('/') {
|
||||
path.to_string()
|
||||
} else {
|
||||
format!("/{path}")
|
||||
};
|
||||
let uri_text = if let Some(query) = query_string.filter(|value| !value.is_empty()) {
|
||||
format!("{normalized_path}?{query}")
|
||||
} else {
|
||||
normalized_path
|
||||
};
|
||||
uri_text.parse::<http::Uri>().map_err(|_| {
|
||||
build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid internal gateway uri",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn infer_legacy_finalize_signature(
|
||||
payload: &crate::gateway::GatewaySyncReportRequest,
|
||||
) -> Option<String> {
|
||||
let report_context = payload.report_context.as_ref()?;
|
||||
let from_context = report_context
|
||||
.get("client_api_format")
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| {
|
||||
report_context
|
||||
.get("provider_api_format")
|
||||
.and_then(Value::as_str)
|
||||
})
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned);
|
||||
if from_context.is_some() {
|
||||
return from_context;
|
||||
}
|
||||
|
||||
let report_kind = payload.report_kind.trim().to_ascii_lowercase();
|
||||
if report_kind.starts_with("openai_chat_") {
|
||||
return Some("openai:chat".to_string());
|
||||
}
|
||||
if report_kind.starts_with("openai_compact_") {
|
||||
return Some("openai:compact".to_string());
|
||||
}
|
||||
if report_kind.starts_with("openai_cli_") {
|
||||
return Some("openai:cli".to_string());
|
||||
}
|
||||
if report_kind.starts_with("openai_video_") {
|
||||
return Some("openai:video".to_string());
|
||||
}
|
||||
if report_kind.starts_with("claude_chat_") {
|
||||
return Some("claude:chat".to_string());
|
||||
}
|
||||
if report_kind.starts_with("claude_cli_") {
|
||||
return Some("claude:cli".to_string());
|
||||
}
|
||||
if report_kind.starts_with("gemini_chat_") {
|
||||
return Some("gemini:chat".to_string());
|
||||
}
|
||||
if report_kind.starts_with("gemini_cli_") {
|
||||
return Some("gemini:cli".to_string());
|
||||
}
|
||||
if report_kind.starts_with("gemini_video_") {
|
||||
return Some("gemini:video".to_string());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn build_legacy_finalize_decision(
|
||||
payload: &crate::gateway::GatewaySyncReportRequest,
|
||||
) -> Option<GatewayControlDecision> {
|
||||
let signature = infer_legacy_finalize_signature(payload)?;
|
||||
let (public_path, route_family, route_kind) = match signature.as_str() {
|
||||
"openai:chat" => ("/v1/chat/completions", "openai", "chat"),
|
||||
"openai:cli" => ("/v1/responses", "openai", "cli"),
|
||||
"openai:compact" => ("/v1/responses/compact", "openai", "compact"),
|
||||
"openai:video" => ("/v1/videos", "openai", "video"),
|
||||
"claude:chat" => ("/v1/messages", "claude", "chat"),
|
||||
"claude:cli" => ("/v1/messages", "claude", "cli"),
|
||||
"gemini:chat" => ("/v1beta/models", "gemini", "chat"),
|
||||
"gemini:cli" => ("/v1beta/models", "gemini", "cli"),
|
||||
"gemini:video" => ("/v1beta/models", "gemini", "video"),
|
||||
_ => return None,
|
||||
};
|
||||
Some(
|
||||
GatewayControlDecision::synthetic(
|
||||
public_path,
|
||||
Some("ai_public".to_string()),
|
||||
Some(route_family.to_string()),
|
||||
Some(route_kind.to_string()),
|
||||
Some(signature),
|
||||
)
|
||||
.with_execution_runtime_candidate(true),
|
||||
)
|
||||
}
|
||||
|
||||
fn build_legacy_finalize_video_plan(
|
||||
payload: &crate::gateway::GatewaySyncReportRequest,
|
||||
) -> Option<aether_contracts::ExecutionPlan> {
|
||||
let signature = infer_legacy_finalize_signature(payload)?;
|
||||
if !matches!(signature.as_str(), "openai:video" | "gemini:video") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let report_context = payload.report_context.as_ref().and_then(Value::as_object);
|
||||
let context_text = |key: &str| {
|
||||
report_context
|
||||
.and_then(|value| value.get(key))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
};
|
||||
|
||||
let provider_name = signature
|
||||
.split(':')
|
||||
.next()
|
||||
.expect("video finalize signature should include provider name")
|
||||
.to_string();
|
||||
let model_name = context_text("model")
|
||||
.or_else(|| context_text("model_name"))
|
||||
.or_else(|| match signature.as_str() {
|
||||
"openai:video" => Some("sora-2".to_string()),
|
||||
"gemini:video" => Some("veo-3".to_string()),
|
||||
_ => None,
|
||||
});
|
||||
let original_request_body = report_context
|
||||
.and_then(|value| value.get("original_request_body"))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| json!({}));
|
||||
let url = match signature.as_str() {
|
||||
"openai:video" => "https://legacy.internal.invalid/v1/videos".to_string(),
|
||||
"gemini:video" => format!(
|
||||
"https://legacy.internal.invalid/v1beta/models/{}:predictLongRunning",
|
||||
model_name.clone().unwrap_or_else(|| "veo-3".to_string())
|
||||
),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(aether_contracts::ExecutionPlan {
|
||||
request_id: context_text("request_id").unwrap_or_else(|| payload.trace_id.clone()),
|
||||
candidate_id: None,
|
||||
provider_name: Some(provider_name.clone()),
|
||||
provider_id: context_text("provider_id")
|
||||
.unwrap_or_else(|| format!("legacy-{provider_name}-video-provider")),
|
||||
endpoint_id: context_text("endpoint_id")
|
||||
.unwrap_or_else(|| format!("legacy-{provider_name}-video-endpoint")),
|
||||
key_id: context_text("key_id")
|
||||
.or_else(|| context_text("api_key_id"))
|
||||
.unwrap_or_else(|| format!("legacy-{provider_name}-video-key")),
|
||||
method: "POST".to_string(),
|
||||
url,
|
||||
headers: std::collections::BTreeMap::from([(
|
||||
"authorization".to_string(),
|
||||
"Bearer legacy-internal-gateway".to_string(),
|
||||
)]),
|
||||
content_type: Some("application/json".to_string()),
|
||||
content_encoding: None,
|
||||
body: aether_contracts::RequestBody::from_json(original_request_body),
|
||||
stream: false,
|
||||
client_api_format: signature.clone(),
|
||||
provider_api_format: signature,
|
||||
model_name,
|
||||
proxy: Some(aether_contracts::ProxySnapshot {
|
||||
enabled: Some(false),
|
||||
mode: Some("direct".to_string()),
|
||||
node_id: None,
|
||||
label: None,
|
||||
url: None,
|
||||
extra: None,
|
||||
}),
|
||||
tls_profile: None,
|
||||
timeouts: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_legacy_finalize_video_request_path(
|
||||
payload: &crate::gateway::GatewaySyncReportRequest,
|
||||
) -> Option<String> {
|
||||
let signature = infer_legacy_finalize_signature(payload)?;
|
||||
let report_context = payload.report_context.as_ref().and_then(Value::as_object);
|
||||
let context_text = |key: &str| {
|
||||
report_context
|
||||
.and_then(|value| value.get(key))
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
};
|
||||
|
||||
match payload.report_kind.as_str() {
|
||||
"openai_video_delete_sync_finalize" => {
|
||||
let task_id = context_text("task_id")?;
|
||||
Some(format!("/v1/videos/{task_id}"))
|
||||
}
|
||||
"openai_video_cancel_sync_finalize" => {
|
||||
let task_id = context_text("task_id")?;
|
||||
Some(format!("/v1/videos/{task_id}/cancel"))
|
||||
}
|
||||
"gemini_video_cancel_sync_finalize" => {
|
||||
let short_id = context_text("task_id")
|
||||
.or_else(|| context_text("local_short_id"))
|
||||
.or_else(|| {
|
||||
context_text("operation_name").and_then(|value| {
|
||||
value
|
||||
.rsplit('/')
|
||||
.next()
|
||||
.map(str::trim)
|
||||
.filter(|inner| !inner.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
})
|
||||
})?;
|
||||
let model = context_text("model")
|
||||
.or_else(|| context_text("model_name"))
|
||||
.unwrap_or_else(|| match signature.as_str() {
|
||||
"gemini:video" => "veo-3".to_string(),
|
||||
_ => "unknown".to_string(),
|
||||
});
|
||||
Some(format!(
|
||||
"/v1beta/models/{model}/operations/{short_id}:cancel"
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn maybe_build_legacy_finalize_video_response(
|
||||
state: &AppState,
|
||||
trace_id: &str,
|
||||
decision: &GatewayControlDecision,
|
||||
payload: &crate::gateway::GatewaySyncReportRequest,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
let Some(plan) = build_legacy_finalize_video_plan(payload) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if let Some(outcome) = crate::gateway::maybe_build_local_video_success_outcome(
|
||||
trace_id,
|
||||
decision,
|
||||
payload,
|
||||
&state.video_tasks,
|
||||
&plan,
|
||||
)? {
|
||||
if let Some(snapshot) = outcome.local_task_snapshot.clone() {
|
||||
state.video_tasks.record_snapshot(snapshot.clone());
|
||||
let _ = state.upsert_video_task_snapshot(&snapshot).await?;
|
||||
}
|
||||
match outcome.report_mode {
|
||||
crate::gateway::video_tasks::VideoTaskSyncReportMode::InlineSync => {
|
||||
crate::gateway::usage::submit_sync_report(state, trace_id, outcome.report_payload)
|
||||
.await?;
|
||||
}
|
||||
crate::gateway::video_tasks::VideoTaskSyncReportMode::Background => {
|
||||
crate::gateway::usage::spawn_sync_report(
|
||||
state.clone(),
|
||||
trace_id.to_string(),
|
||||
outcome.report_payload,
|
||||
);
|
||||
}
|
||||
}
|
||||
let mut response = outcome.response;
|
||||
response.headers_mut().insert(
|
||||
HeaderName::from_static(CONTROL_EXECUTED_HEADER),
|
||||
HeaderValue::from_static("true"),
|
||||
);
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
if let Some(mut response) =
|
||||
crate::gateway::maybe_build_local_sync_finalize_response(trace_id, decision, payload)?
|
||||
{
|
||||
if let Some(request_path) = build_legacy_finalize_video_request_path(payload) {
|
||||
state
|
||||
.video_tasks
|
||||
.apply_finalize_mutation(request_path.as_str(), payload.report_kind.as_str());
|
||||
if let Some(snapshot) = state
|
||||
.video_tasks
|
||||
.snapshot_for_route(decision.route_family.as_deref(), request_path.as_str())
|
||||
{
|
||||
let _ = state.upsert_video_task_snapshot(&snapshot).await?;
|
||||
}
|
||||
}
|
||||
if let Some(success_report_kind) =
|
||||
crate::gateway::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;
|
||||
crate::gateway::usage::spawn_sync_report(
|
||||
state.clone(),
|
||||
trace_id.to_string(),
|
||||
report_payload,
|
||||
);
|
||||
}
|
||||
response.headers_mut().insert(
|
||||
HeaderName::from_static(CONTROL_EXECUTED_HEADER),
|
||||
HeaderValue::from_static("true"),
|
||||
);
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
if let Some(mut response) =
|
||||
crate::gateway::maybe_build_local_video_error_response(trace_id, decision, payload)?
|
||||
{
|
||||
if let Some(error_report_kind) =
|
||||
crate::gateway::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;
|
||||
crate::gateway::usage::spawn_sync_report(
|
||||
state.clone(),
|
||||
trace_id.to_string(),
|
||||
report_payload,
|
||||
);
|
||||
}
|
||||
response.headers_mut().insert(
|
||||
HeaderName::from_static(CONTROL_EXECUTED_HEADER),
|
||||
HeaderValue::from_static("true"),
|
||||
);
|
||||
return Ok(Some(response));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(crate) fn gateway_error_message(error: GatewayError) -> String {
|
||||
match error {
|
||||
GatewayError::UpstreamUnavailable { message, .. }
|
||||
| GatewayError::ControlUnavailable { message, .. }
|
||||
| GatewayError::Internal(message) => message,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_internal_tunnel_heartbeat_ack(node: &StoredProxyNode) -> serde_json::Value {
|
||||
let Some(remote_config) = node.remote_config.as_ref() else {
|
||||
return json!({});
|
||||
};
|
||||
|
||||
let mut payload = serde_json::Map::new();
|
||||
payload.insert("remote_config".to_string(), remote_config.clone());
|
||||
payload.insert("config_version".to_string(), json!(node.config_version));
|
||||
if let Some(upgrade_to) = remote_config
|
||||
.as_object()
|
||||
.and_then(|value| value.get("upgrade_to"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
payload.insert("upgrade_to".to_string(), json!(upgrade_to));
|
||||
}
|
||||
serde_json::Value::Object(payload)
|
||||
}
|
||||
|
||||
pub(crate) fn parse_internal_tunnel_heartbeat_request(
|
||||
request_body: &[u8],
|
||||
) -> Result<InternalTunnelHeartbeatRequest, Response<Body>> {
|
||||
let payload =
|
||||
serde_json::from_slice::<InternalTunnelHeartbeatRequest>(request_body).map_err(|_| {
|
||||
build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid heartbeat payload",
|
||||
)
|
||||
})?;
|
||||
|
||||
let node_id = payload.node_id.trim();
|
||||
if node_id.is_empty() || node_id.len() > 36 {
|
||||
return Err(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid heartbeat payload",
|
||||
));
|
||||
}
|
||||
if payload
|
||||
.heartbeat_interval
|
||||
.is_some_and(|value| !(5..=600).contains(&value))
|
||||
|| payload.active_connections.is_some_and(|value| value < 0)
|
||||
|| payload.total_requests.is_some_and(|value| value < 0)
|
||||
|| payload.avg_latency_ms.is_some_and(|value| value < 0.0)
|
||||
|| payload.failed_requests.is_some_and(|value| value < 0)
|
||||
|| payload.dns_failures.is_some_and(|value| value < 0)
|
||||
|| payload.stream_errors.is_some_and(|value| value < 0)
|
||||
|| payload
|
||||
.proxy_version
|
||||
.as_deref()
|
||||
.is_some_and(|value| value.chars().count() > 20)
|
||||
|| payload
|
||||
.proxy_metadata
|
||||
.as_ref()
|
||||
.is_some_and(|value| !value.is_object())
|
||||
{
|
||||
return Err(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid heartbeat payload",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
pub(crate) fn parse_internal_tunnel_node_status_request(
|
||||
request_body: &[u8],
|
||||
) -> Result<InternalTunnelNodeStatusRequest, Response<Body>> {
|
||||
let payload =
|
||||
serde_json::from_slice::<InternalTunnelNodeStatusRequest>(request_body).map_err(|_| {
|
||||
build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid node-status payload",
|
||||
)
|
||||
})?;
|
||||
|
||||
let node_id = payload.node_id.trim();
|
||||
if node_id.is_empty() || node_id.len() > 36 || payload.conn_count < 0 {
|
||||
return Err(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"invalid node-status payload",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
fn build_management_token_user_payload(
|
||||
user: &StoredManagementTokenUserSummary,
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
"id": user.id,
|
||||
"email": user.email,
|
||||
"username": user.username,
|
||||
"role": user.role,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn build_management_token_payload(
|
||||
token: &StoredManagementToken,
|
||||
user: Option<&StoredManagementTokenUserSummary>,
|
||||
) -> serde_json::Value {
|
||||
let mut payload = json!({
|
||||
"id": token.id,
|
||||
"user_id": token.user_id,
|
||||
"name": token.name,
|
||||
"description": token.description,
|
||||
"token_display": token.token_display(),
|
||||
"allowed_ips": token.allowed_ips,
|
||||
"expires_at": token.expires_at_unix_secs.and_then(unix_secs_to_rfc3339),
|
||||
"last_used_at": token.last_used_at_unix_secs.and_then(unix_secs_to_rfc3339),
|
||||
"last_used_ip": token.last_used_ip,
|
||||
"usage_count": token.usage_count,
|
||||
"is_active": token.is_active,
|
||||
"created_at": token.created_at_unix_secs.and_then(unix_secs_to_rfc3339),
|
||||
"updated_at": token.updated_at_unix_secs.and_then(unix_secs_to_rfc3339),
|
||||
});
|
||||
if let Some(user) = user {
|
||||
payload["user"] = build_management_token_user_payload(user);
|
||||
}
|
||||
payload
|
||||
}
|
||||
Reference in New Issue
Block a user