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

将 gateway 内部的 model-fetch、provider-transport、scheduler-core、
usage-runtime、video-tasks-core 模块提取为独立 crate;重构 gateway
内部模块结构(state/router/cache/data/query 等);移除大量遗留模块
文件;新增 systemd 二进制部署骨架及相关文档;更新前端 usage 相关
API 和组件。
This commit is contained in:
fawney19
2026-04-05 20:23:16 +08:00
parent cbc811f6ce
commit 763ff03a7b
777 changed files with 42659 additions and 21469 deletions

View File

@@ -8,19 +8,29 @@ use super::{
maybe_build_internal_finalize_video_response, parse_internal_tunnel_heartbeat_request,
parse_internal_tunnel_node_status_request,
};
use crate::gateway::ai_pipeline::planner as ai_planner;
use crate::gateway::constants::{
use crate::ai_pipeline::finalize::maybe_build_sync_finalize_outcome;
use crate::ai_pipeline::planner as ai_planner;
use crate::ai_pipeline::planner::{
maybe_build_stream_plan_payload, maybe_build_sync_plan_payload,
};
use crate::constants::{
CONTROL_EXECUTED_HEADER, EXECUTION_PATH_EXECUTION_RUNTIME_STREAM,
EXECUTION_PATH_EXECUTION_RUNTIME_SYNC,
};
use crate::gateway::handlers::{
use crate::control::GatewayControlDecision;
use crate::control::GatewayPublicRequestContext;
use crate::execution_runtime::{
execute_execution_runtime_stream, execute_execution_runtime_sync,
};
use crate::handlers::{
InternalGatewayAuthContextRequest, InternalGatewayExecuteRequest, InternalGatewayResolveRequest,
};
use crate::gateway::{
execute_execution_runtime_stream, execute_execution_runtime_sync,
maybe_build_stream_plan_payload, maybe_build_sync_finalize_outcome,
maybe_build_sync_plan_payload, AppState, GatewayControlDecision, GatewayError,
GatewayPublicRequestContext, ProxyNodeHeartbeatMutation, ProxyNodeTunnelStatusMutation,
use crate::tunnel::{
is_tunnel_heartbeat_path, is_tunnel_node_status_path, TUNNEL_ROUTE_FAMILY,
};
use crate::{AppState, GatewayError};
use aether_data::repository::proxy_nodes::{
ProxyNodeHeartbeatMutation, ProxyNodeTunnelStatusMutation,
};
use axum::body::{Body, Bytes};
use axum::http::{self, HeaderName, HeaderValue, Response};
@@ -85,7 +95,7 @@ pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
Ok(uri) => uri,
Err(response) => return Ok(Some(response)),
};
let resolved = crate::gateway::control::resolve_control_route(
let resolved = crate::control::resolve_control_route(
state,
&method,
&uri,
@@ -137,7 +147,7 @@ pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
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(
let auth_context = crate::control::resolve_execution_runtime_auth_context(
state,
&synthetic_decision,
&headers,
@@ -191,7 +201,7 @@ pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
.as_object()
.map(|value| value.is_empty())
.unwrap_or(false);
let Some(mut resolved) = crate::gateway::resolve_control_route(
let Some(mut resolved) = crate::control::resolve_control_route(
state,
&parts.method,
&parts.uri,
@@ -293,7 +303,7 @@ pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
.as_object()
.map(|value| value.is_empty())
.unwrap_or(false);
let Some(mut resolved) = crate::gateway::resolve_control_route(
let Some(mut resolved) = crate::control::resolve_control_route(
state,
&parts.method,
&parts.uri,
@@ -393,7 +403,7 @@ pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
.as_object()
.map(|value| value.is_empty())
.unwrap_or(false);
let Some(mut resolved) = crate::gateway::resolve_control_route(
let Some(mut resolved) = crate::control::resolve_control_route(
state,
&parts.method,
&parts.uri,
@@ -459,7 +469,7 @@ pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
.as_deref()
.unwrap_or(request_context.trace_id.as_str())
.to_string();
let Some(mut resolved) = crate::gateway::resolve_control_route(
let Some(mut resolved) = crate::control::resolve_control_route(
state,
&parts.method,
&parts.uri,
@@ -529,7 +539,7 @@ pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
.as_object()
.map(|value| value.is_empty())
.unwrap_or(false);
let Some(mut resolved) = crate::gateway::resolve_control_route(
let Some(mut resolved) = crate::control::resolve_control_route(
state,
&parts.method,
&parts.uri,
@@ -613,7 +623,7 @@ pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
.as_deref()
.unwrap_or(request_context.trace_id.as_str())
.to_string();
let Some(mut resolved) = crate::gateway::resolve_control_route(
let Some(mut resolved) = crate::control::resolve_control_route(
state,
&parts.method,
&parts.uri,
@@ -670,9 +680,10 @@ pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
"invalid internal gateway report-sync payload",
)));
};
let payload = match serde_json::from_slice::<crate::gateway::GatewaySyncReportRequest>(
request_body,
) {
let payload = match serde_json::from_slice::<
crate::usage::GatewaySyncReportRequest,
>(request_body)
{
Ok(payload) => payload,
Err(_) => {
return Ok(Some(build_internal_control_error_response(
@@ -682,7 +693,7 @@ pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
}
};
let trace_id = payload.trace_id.clone();
crate::gateway::usage::submit_sync_report(state, &trace_id, payload).await?;
crate::usage::submit_sync_report(state, &trace_id, payload).await?;
return Ok(Some(Json(json!({ "ok": true })).into_response()));
}
Some("report_stream")
@@ -695,7 +706,7 @@ pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
)));
};
let payload = match serde_json::from_slice::<
crate::gateway::GatewayStreamReportRequest,
crate::usage::GatewayStreamReportRequest,
>(request_body)
{
Ok(payload) => payload,
@@ -707,7 +718,7 @@ pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
}
};
let trace_id = payload.trace_id.clone();
crate::gateway::usage::submit_stream_report(state, &trace_id, payload).await?;
crate::usage::submit_stream_report(state, &trace_id, payload).await?;
return Ok(Some(Json(json!({ "ok": true })).into_response()));
}
Some("finalize_sync")
@@ -719,9 +730,10 @@ pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
"invalid internal gateway finalize-sync payload",
)));
};
let payload = match serde_json::from_slice::<crate::gateway::GatewaySyncReportRequest>(
request_body,
) {
let payload = match serde_json::from_slice::<
crate::usage::GatewaySyncReportRequest,
>(request_body)
{
Ok(payload) => payload,
Err(_) => {
return Ok(Some(build_internal_control_error_response(
@@ -741,7 +753,7 @@ pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
maybe_build_sync_finalize_outcome(&trace_id, &synthetic_decision, &payload)?
{
if let Some(background_report) = outcome.background_report {
crate::gateway::usage::spawn_sync_report(
crate::usage::spawn_sync_report(
state.clone(),
trace_id.clone(),
background_report,
@@ -784,16 +796,14 @@ pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
)));
}
if decision.route_family.as_deref() != Some(crate::gateway::TUNNEL_ROUTE_FAMILY)
if decision.route_family.as_deref() != Some(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()) =>
{
Some("heartbeat") if 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,
@@ -832,9 +842,7 @@ pub(crate) async fn maybe_build_local_internal_proxy_response_impl(
return Ok(Some(response));
}
Some("node_status")
if crate::gateway::is_tunnel_node_status_path(
request_context.request_path.as_str(),
) =>
if 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(

View File

@@ -1,4 +1,4 @@
use crate::gateway::constants::{
use crate::constants::{
CONTROL_ACTION_HEADER, CONTROL_ACTION_PROXY_PUBLIC, CONTROL_EXECUTED_HEADER,
CONTROL_EXECUTION_RUNTIME_CANDIDATE_KEY, EXECUTION_PATH_CONTROL_EXECUTE_STREAM,
EXECUTION_PATH_CONTROL_EXECUTE_SYNC, EXECUTION_PATH_DISTRIBUTED_OVERLOADED,
@@ -6,14 +6,25 @@ use crate::gateway::constants::{
EXECUTION_PATH_HEADER, EXECUTION_PATH_LOCAL_AUTH_DENIED, EXECUTION_PATH_LOCAL_OVERLOADED,
EXECUTION_PATH_LOCAL_RATE_LIMITED, EXECUTION_PATH_PUBLIC_PROXY_PASSTHROUGH,
};
use crate::gateway::handlers::admin::build_internal_control_error_response;
use crate::gateway::handlers::{
use crate::control::GatewayControlDecision;
use crate::execution_runtime::{
maybe_build_local_sync_finalize_response, maybe_build_local_video_error_response,
maybe_build_local_video_success_outcome, resolve_local_sync_error_background_report_kind,
resolve_local_sync_success_background_report_kind,
};
use crate::handlers::admin::provider_oauth_refresh::build_internal_control_error_response;
use crate::handlers::{
unix_secs_to_rfc3339, InternalTunnelHeartbeatRequest, InternalTunnelNodeStatusRequest,
};
use crate::gateway::{AppState, GatewayControlDecision, GatewayError, StoredProxyNode};
use crate::video_tasks::{
build_internal_finalize_video_plan, build_local_sync_finalize_request_path,
};
use crate::{AppState, GatewayError};
use aether_data::repository::management_tokens::{
StoredManagementToken, StoredManagementTokenUserSummary,
};
use aether_data::repository::proxy_nodes::StoredProxyNode;
use aether_usage_runtime::{infer_internal_finalize_signature, resolve_internal_finalize_route};
use axum::body::Body;
use axum::http::{self, header::HeaderName, header::HeaderValue, Response};
use axum::response::IntoResponse;
@@ -76,7 +87,7 @@ pub(crate) fn build_internal_gateway_resolve_payload(
}
pub(crate) fn build_internal_gateway_fallback_plan_payload(
auth_context: Option<&crate::gateway::GatewayControlAuthContext>,
auth_context: Option<&crate::control::GatewayControlAuthContext>,
) -> serde_json::Value {
let mut payload = serde_json::Map::new();
payload.insert("action".to_string(), json!("fallback_plan"));
@@ -212,225 +223,40 @@ pub(crate) fn build_internal_gateway_uri(
})
}
fn infer_internal_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_internal_finalize_decision(
payload: &crate::gateway::GatewaySyncReportRequest,
payload: &crate::usage::GatewaySyncReportRequest,
) -> Option<GatewayControlDecision> {
let signature = infer_internal_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,
};
let route = resolve_internal_finalize_route(signature.as_str())?;
Some(
GatewayControlDecision::synthetic(
public_path,
route.public_path,
Some("ai_public".to_string()),
Some(route_family.to_string()),
Some(route_kind.to_string()),
Some(route.route_family.to_string()),
Some(route.route_kind.to_string()),
Some(signature),
)
.with_execution_runtime_candidate(true),
)
}
fn build_internal_finalize_video_plan(
payload: &crate::gateway::GatewaySyncReportRequest,
) -> Option<aether_contracts::ExecutionPlan> {
let signature = infer_internal_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://internal.gateway.invalid/v1/videos".to_string(),
"gemini:video" => format!(
"https://internal.gateway.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!("internal-{provider_name}-video-provider")),
endpoint_id: context_text("endpoint_id")
.unwrap_or_else(|| format!("internal-{provider_name}-video-endpoint")),
key_id: context_text("key_id")
.or_else(|| context_text("api_key_id"))
.unwrap_or_else(|| format!("internal-{provider_name}-video-key")),
method: "POST".to_string(),
url,
headers: std::collections::BTreeMap::from([(
"authorization".to_string(),
"Bearer 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_internal_finalize_video_request_path(
payload: &crate::gateway::GatewaySyncReportRequest,
) -> Option<String> {
let signature = infer_internal_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_internal_finalize_video_response(
state: &AppState,
trace_id: &str,
decision: &GatewayControlDecision,
payload: &crate::gateway::GatewaySyncReportRequest,
payload: &crate::usage::GatewaySyncReportRequest,
) -> Result<Option<Response<Body>>, GatewayError> {
let Some(plan) = build_internal_finalize_video_plan(payload) else {
let Some(plan) = infer_internal_finalize_signature(payload).and_then(|signature| {
build_internal_finalize_video_plan(
payload.trace_id.as_str(),
signature.as_str(),
payload.report_context.as_ref(),
)
}) else {
return Ok(None);
};
if let Some(outcome) = crate::gateway::maybe_build_local_video_success_outcome(
if let Some(outcome) = maybe_build_local_video_success_outcome(
trace_id,
decision,
payload,
@@ -442,12 +268,12 @@ pub(crate) async fn maybe_build_internal_finalize_video_response(
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)
crate::video_tasks::VideoTaskSyncReportMode::InlineSync => {
crate::usage::submit_sync_report(state, trace_id, outcome.report_payload)
.await?;
}
crate::gateway::video_tasks::VideoTaskSyncReportMode::Background => {
crate::gateway::usage::spawn_sync_report(
crate::video_tasks::VideoTaskSyncReportMode::Background => {
crate::usage::spawn_sync_report(
state.clone(),
trace_id.to_string(),
outcome.report_payload,
@@ -463,9 +289,16 @@ pub(crate) async fn maybe_build_internal_finalize_video_response(
}
if let Some(mut response) =
crate::gateway::maybe_build_local_sync_finalize_response(trace_id, decision, payload)?
maybe_build_local_sync_finalize_response(trace_id, decision, payload)?
{
if let Some(request_path) = build_internal_finalize_video_request_path(payload) {
let request_path = infer_internal_finalize_signature(payload).and_then(|signature| {
build_local_sync_finalize_request_path(
payload.report_kind.as_str(),
signature.as_str(),
payload.report_context.as_ref(),
)
});
if let Some(request_path) = request_path {
state
.video_tasks
.apply_finalize_mutation(request_path.as_str(), payload.report_kind.as_str());
@@ -477,13 +310,11 @@ pub(crate) async fn maybe_build_internal_finalize_video_response(
}
}
if let Some(success_report_kind) =
crate::gateway::resolve_local_sync_success_background_report_kind(
payload.report_kind.as_str(),
)
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(
report_payload.report_kind = success_report_kind.to_string();
crate::usage::spawn_sync_report(
state.clone(),
trace_id.to_string(),
report_payload,
@@ -496,17 +327,14 @@ pub(crate) async fn maybe_build_internal_finalize_video_response(
return Ok(Some(response));
}
if let Some(mut response) =
crate::gateway::maybe_build_local_video_error_response(trace_id, decision, payload)?
if let Some(mut response) = 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(),
)
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(
report_payload.report_kind = error_report_kind.to_string();
crate::usage::spawn_sync_report(
state.clone(),
trace_id.to_string(),
report_payload,

View File

@@ -1,4 +1,4 @@
use super::admin::build_internal_control_error_response;
use super::admin::provider_oauth_refresh::build_internal_control_error_response;
mod gateway_helpers;
use self::gateway_helpers::*;