feat(gateway): 重构 usage 数据层、迁移系统与系统导入

数据库迁移:
- 引入 baseline v2 bootstrap,空库首次启动自动初始化
- 服务启动不再自动执行迁移,需显式 `--migrate` 运行
- 新增 pending migration 检测,schema 落后时拒绝启动

Usage 数据层:
- usage body 存储外部化为独立 blob 表
- 新增 HTTP audit 表拆分存储请求/响应头与 body ref
- 后台清理任务支持 legacy body ref 元数据迁移
- usage runtime 写入迁移到专用 tokio runtime(独立线程池, 8MB 栈)

系统导入/导出:
- 支持用户、API Keys、钱包数据的完整导入
- 兼容 legacy 与 v1.3+ 两种导出格式

其他改进:
- executor outcome 增加 runtime miss 诊断上下文
- 主 tokio runtime 栈大小调整为 8MB
- 前端 provider 管理支持 base URL 配置
- dev.sh 支持 --migrate 参数
This commit is contained in:
fawney19
2026-04-13 14:01:22 +08:00
parent 3698e5a833
commit 5bb08e6aa4
106 changed files with 21736 additions and 1529 deletions

View File

@@ -6,6 +6,10 @@ use aether_data_contracts::repository::candidates::RequestCandidateStatus;
use aether_scheduler_core::{
parse_request_candidate_report_context, SchedulerRequestCandidateStatusUpdate,
};
use aether_usage_runtime::{
build_lifecycle_usage_seed, build_stream_terminal_usage_payload_seed,
build_sync_terminal_usage_payload_seed, build_terminal_usage_context_seed,
};
use async_stream::stream;
use axum::body::{Body, Bytes};
use axum::http::Response;
@@ -62,6 +66,36 @@ use crate::usage::submit_stream_report;
use crate::usage::{GatewayStreamReportRequest, GatewaySyncReportRequest};
use crate::{AppState, GatewayError};
fn record_sync_terminal_usage(
state: &AppState,
plan: &ExecutionPlan,
report_context: Option<&serde_json::Value>,
payload: &GatewaySyncReportRequest,
) {
let context_seed = build_terminal_usage_context_seed(plan, report_context);
let payload_seed = build_sync_terminal_usage_payload_seed(payload);
state
.usage_runtime
.record_sync_terminal(state.data.as_ref(), &context_seed, &payload_seed);
}
fn record_stream_terminal_usage(
state: &AppState,
plan: &ExecutionPlan,
report_context: Option<&serde_json::Value>,
payload: &GatewayStreamReportRequest,
cancelled: bool,
) {
let context_seed = build_terminal_usage_context_seed(plan, report_context);
let payload_seed = build_stream_terminal_usage_payload_seed(payload);
state.usage_runtime.record_stream_terminal(
state.data.as_ref(),
&context_seed,
&payload_seed,
cancelled,
);
}
#[allow(clippy::too_many_arguments)] // internal function, grouping would add unnecessary indirection
pub(crate) async fn execute_execution_runtime_stream(
state: &AppState,
@@ -73,10 +107,10 @@ pub(crate) async fn execute_execution_runtime_stream(
mut report_context: Option<serde_json::Value>,
) -> Result<Option<Response<Body>>, GatewayError> {
ensure_execution_request_candidate_slot(state, &mut plan, &mut report_context).await;
let lifecycle_seed = build_lifecycle_usage_seed(&plan, report_context.as_ref());
state
.usage_runtime
.record_pending(state.data.as_ref(), &plan, report_context.as_ref())
.await;
.record_pending(state.data.as_ref(), &lifecycle_seed);
let plan_request_id_for_log = short_request_id(plan.request_id.as_str());
let provider_name = plan.provider_name.as_deref().unwrap_or("-");
let endpoint_id = plan.endpoint_id.as_str();
@@ -348,6 +382,7 @@ async fn execute_stream_from_frame_stream(
let candidate_id = plan.candidate_id.as_deref();
let provider_name = plan.provider_name.as_deref().unwrap_or("-");
let model_name = plan.model_name.as_deref().unwrap_or("-");
let lifecycle_seed = build_lifecycle_usage_seed(&plan, report_context.as_ref());
let candidate_index = parse_request_candidate_report_context(report_context.as_ref())
.and_then(|context| context.candidate_index)
.map(|value| value.to_string())
@@ -527,15 +562,7 @@ async fn execute_stream_from_frame_stream(
body_base64: body_base64.clone(),
telemetry: None,
};
state
.usage_runtime
.record_sync_terminal(
state.data.as_ref(),
&plan,
report_context.as_ref(),
&usage_payload,
)
.await;
record_sync_terminal_usage(state, &plan, report_context.as_ref(), &usage_payload);
let terminal_unix_secs = current_request_candidate_unix_ms();
record_local_request_candidate_status(
state,
@@ -739,15 +766,12 @@ async fn execute_stream_from_frame_stream(
body_base64: None,
telemetry: prefetched_telemetry.clone(),
};
state
.usage_runtime
.record_sync_terminal(
state.data.as_ref(),
&plan,
report_context.as_ref(),
&payload,
)
.await;
record_sync_terminal_usage(
state,
&plan,
report_context.as_ref(),
&payload,
);
let response = submit_local_core_error_or_sync_finalize(
state, trace_id, decision, payload,
)
@@ -876,17 +900,12 @@ async fn execute_stream_from_frame_stream(
}
let candidate_started_unix_secs = current_request_candidate_unix_ms();
state
.usage_runtime
.record_stream_started(
state.data.as_ref(),
&plan,
report_context.as_ref(),
status_code,
&headers,
prefetched_telemetry.as_ref(),
)
.await;
state.usage_runtime.record_stream_started(
state.data.as_ref(),
&lifecycle_seed,
status_code,
prefetched_telemetry.as_ref(),
);
record_local_request_candidate_status(
state,
&plan,
@@ -912,6 +931,7 @@ async fn execute_stream_from_frame_stream(
let headers_for_report = headers.clone();
let report_kind_owned = report_kind.clone();
let report_context_owned = report_context.clone();
let lifecycle_seed_for_report = lifecycle_seed.clone();
let provider_prefetched_body_for_report = provider_prefetched_body.clone();
let prefetched_body_for_report = prefetched_body.clone();
let prefetched_chunks_for_body = prefetched_chunks.clone();
@@ -1066,17 +1086,12 @@ async fn execute_stream_from_frame_stream(
);
telemetry = Some(frame_telemetry.clone());
if should_refresh_stream_usage {
state_for_report
.usage_runtime
.record_stream_started(
state_for_report.data.as_ref(),
&plan_for_report,
report_context_owned.as_ref(),
status_code,
&headers_for_report,
Some(&frame_telemetry),
)
.await;
state_for_report.usage_runtime.record_stream_started(
state_for_report.data.as_ref(),
&lifecycle_seed_for_report,
status_code,
Some(&frame_telemetry),
);
usage_stream_telemetry = Some(frame_telemetry);
}
}
@@ -1230,30 +1245,25 @@ async fn execute_stream_from_frame_stream(
trace_id = %trace_id_owned,
"gateway skipped stream report because downstream disconnected before completion"
);
state_for_report
.usage_runtime
.record_stream_terminal(
state_for_report.data.as_ref(),
&plan_for_report,
report_context_owned.as_ref(),
&GatewayStreamReportRequest {
trace_id: trace_id_owned.clone(),
report_kind: report_kind_owned.clone().unwrap_or_default(),
report_context: report_context_owned.clone(),
status_code: 499,
headers: headers_for_report.clone(),
provider_body_base64: (!provider_buffered_body.is_empty()).then(|| {
base64::engine::general_purpose::STANDARD
.encode(&provider_buffered_body)
}),
client_body_base64: (!buffered_body.is_empty()).then(|| {
base64::engine::general_purpose::STANDARD.encode(&buffered_body)
}),
telemetry: telemetry.clone(),
},
true,
)
.await;
record_stream_terminal_usage(
&state_for_report,
&plan_for_report,
report_context_owned.as_ref(),
&GatewayStreamReportRequest {
trace_id: trace_id_owned.clone(),
report_kind: report_kind_owned.clone().unwrap_or_default(),
report_context: report_context_owned.clone(),
status_code: 499,
headers: headers_for_report.clone(),
provider_body_base64: (!provider_buffered_body.is_empty()).then(|| {
base64::engine::general_purpose::STANDARD.encode(&provider_buffered_body)
}),
client_body_base64: (!buffered_body.is_empty())
.then(|| base64::engine::general_purpose::STANDARD.encode(&buffered_body)),
telemetry: telemetry.clone(),
},
true,
);
record_local_request_candidate_status(
&state_for_report,
&plan_for_report,
@@ -1301,16 +1311,13 @@ async fn execute_stream_from_frame_stream(
.then(|| base64::engine::general_purpose::STANDARD.encode(&buffered_body)),
telemetry: telemetry.clone(),
};
state_for_report
.usage_runtime
.record_stream_terminal(
state_for_report.data.as_ref(),
&plan_for_report,
report_context_owned.as_ref(),
&usage_payload,
false,
)
.await;
record_stream_terminal_usage(
&state_for_report,
&plan_for_report,
report_context_owned.as_ref(),
&usage_payload,
false,
);
record_local_request_candidate_status(
&state_for_report,
&plan_for_report,

View File

@@ -1,6 +1,9 @@
use aether_contracts::{ExecutionError, ExecutionPlan, ExecutionTelemetry};
use aether_data_contracts::repository::candidates::RequestCandidateStatus;
use aether_scheduler_core::SchedulerRequestCandidateStatusUpdate;
use aether_usage_runtime::{
build_sync_terminal_usage_payload_seed, build_terminal_usage_context_seed,
};
use axum::body::Body;
use axum::http::Response;
use base64::Engine as _;
@@ -119,10 +122,11 @@ async fn record_stream_sync_failure(
failure: &StreamFailureReport,
started_at_unix_ms: Option<u64>,
) {
let context_seed = build_terminal_usage_context_seed(plan, report_context);
let payload_seed = build_sync_terminal_usage_payload_seed(payload);
state
.usage_runtime
.record_sync_terminal(state.data.as_ref(), plan, report_context, payload)
.await;
.record_sync_terminal(state.data.as_ref(), &context_seed, &payload_seed);
let terminal_unix_secs = current_request_candidate_unix_ms();
record_report_request_candidate_status(
state,

View File

@@ -6,6 +6,10 @@ use aether_scheduler_core::{
execution_error_details, parse_request_candidate_report_context,
SchedulerRequestCandidateStatusUpdate,
};
use aether_usage_runtime::{
build_lifecycle_usage_seed, build_sync_terminal_usage_payload_seed,
build_terminal_usage_context_seed,
};
use axum::body::Body;
use axum::http::Response;
use base64::Engine as _;
@@ -55,16 +59,17 @@ struct ImplicitSyncFinalizeOutcome {
outcome: LocalCoreSyncFinalizeOutcome,
}
async fn record_sync_terminal_usage(
fn record_sync_terminal_usage(
state: &AppState,
plan: &ExecutionPlan,
report_context: Option<&serde_json::Value>,
payload: &GatewaySyncReportRequest,
) {
let context_seed = build_terminal_usage_context_seed(plan, report_context);
let payload_seed = build_sync_terminal_usage_payload_seed(payload);
state
.usage_runtime
.record_sync_terminal(state.data.as_ref(), plan, report_context, payload)
.await;
.record_sync_terminal(state.data.as_ref(), &context_seed, &payload_seed);
}
#[cfg(test)]
@@ -98,10 +103,10 @@ pub(crate) async fn execute_execution_runtime_sync(
.map(|value| value.to_string())
.unwrap_or_else(|| "-".to_string());
let candidate_started_unix_secs = current_request_candidate_unix_ms();
let lifecycle_seed = build_lifecycle_usage_seed(&plan, report_context.as_ref());
state
.usage_runtime
.record_pending(state.data.as_ref(), &plan, report_context.as_ref())
.await;
.record_pending(state.data.as_ref(), &lifecycle_seed);
#[cfg(not(test))]
let result = {
match DirectSyncExecutionRuntime::new()
@@ -349,7 +354,7 @@ pub(crate) async fn execute_execution_runtime_sync(
.background_report
.as_ref()
.unwrap_or(&implicit_finalize.payload);
record_sync_terminal_usage(state, &plan, report_context.as_ref(), usage_payload).await;
record_sync_terminal_usage(state, &plan, report_context.as_ref(), usage_payload);
if let Some(report_payload) = implicit_finalize.outcome.background_report {
spawn_sync_report(state.clone(), trace_id.to_string(), report_payload);
} else {
@@ -386,8 +391,7 @@ pub(crate) async fn execute_execution_runtime_sync(
&plan,
payload.report_context.as_ref(),
usage_payload,
)
.await;
);
if let Some(report_payload) = outcome.background_report {
spawn_sync_report(state.clone(), trace_id.to_string(), report_payload);
} else {
@@ -417,8 +421,7 @@ pub(crate) async fn execute_execution_runtime_sync(
&plan,
payload.report_context.as_ref(),
&outcome.report_payload,
)
.await;
);
if let Some(snapshot) = outcome.local_task_snapshot.clone() {
state.video_tasks.record_snapshot(snapshot.clone());
let _ = state.upsert_video_task_snapshot(&snapshot).await?;
@@ -454,8 +457,7 @@ pub(crate) async fn execute_execution_runtime_sync(
&plan,
payload.report_context.as_ref(),
&usage_payload,
)
.await;
);
state
.video_tasks
.apply_finalize_mutation(request_path, payload.report_kind.as_str());
@@ -505,8 +507,7 @@ pub(crate) async fn execute_execution_runtime_sync(
&plan,
payload.report_context.as_ref(),
&usage_payload,
)
.await;
);
if let Some(error_report_kind) =
resolve_local_sync_error_background_report_kind(payload.report_kind.as_str())
{
@@ -530,7 +531,7 @@ pub(crate) async fn execute_execution_runtime_sync(
candidate_id,
)?));
}
record_sync_terminal_usage(state, &plan, payload.report_context.as_ref(), &payload).await;
record_sync_terminal_usage(state, &plan, payload.report_context.as_ref(), &payload);
let response =
submit_local_core_error_or_sync_finalize(state, trace_id, decision, payload).await?;
return Ok(Some(attach_control_metadata_headers(
@@ -540,7 +541,7 @@ pub(crate) async fn execute_execution_runtime_sync(
)?));
}
record_sync_terminal_usage(state, &plan, report_context.as_ref(), &base_usage_payload).await;
record_sync_terminal_usage(state, &plan, report_context.as_ref(), &base_usage_payload);
if let Some(report_kind) = report_kind {
let report = GatewaySyncReportRequest {
trace_id: trace_id.to_string(),