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

将 gateway 内部的 model-fetch、provider-transport、scheduler-core、
usage-runtime、video-tasks-core 模块提取为独立 crate;重构 gateway
内部模块结构(state/router/cache/data/query 等);移除大量遗留模块
文件;新增 systemd 二进制部署骨架及相关文档;更新前端 usage 相关
API 和组件。
This commit is contained in:
fawney19
2026-04-05 20:23:16 +08:00
parent cbc811f6ce
commit 763ff03a7b
777 changed files with 42654 additions and 21464 deletions
@@ -0,0 +1,664 @@
use aether_contracts::{ExecutionError, ExecutionPlan};
use aether_data::repository::candidates::{
RequestCandidateStatus, StoredRequestCandidate, UpsertRequestCandidateRecord,
};
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SchedulerRequestCandidateReportContext {
pub request_id: Option<String>,
pub candidate_id: Option<String>,
pub user_id: Option<String>,
pub api_key_id: Option<String>,
pub candidate_index: Option<u32>,
pub retry_index: u32,
pub provider_id: Option<String>,
pub endpoint_id: Option<String>,
pub key_id: Option<String>,
pub client_api_format: Option<String>,
pub provider_api_format: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SchedulerResolvedReportRequestCandidateSlot {
pub id: String,
pub request_id: String,
pub user_id: Option<String>,
pub api_key_id: Option<String>,
pub candidate_index: u32,
pub retry_index: u32,
pub provider_id: Option<String>,
pub endpoint_id: Option<String>,
pub key_id: Option<String>,
pub extra_data: Option<Value>,
pub created_at_unix_secs: u64,
pub started_at_unix_secs: Option<u64>,
pub finished_at_unix_secs: Option<u64>,
}
pub struct SchedulerExecutionRequestCandidateSeed {
pub upsert_record: UpsertRequestCandidateRecord,
pub report_context: Value,
}
pub fn execution_error_details(
error: Option<&ExecutionError>,
body_json: Option<&Value>,
) -> (Option<String>, Option<String>) {
match error {
Some(error) => (
Some(format!("{:?}", error.kind)),
Some(error.message.trim().to_string()).filter(|value| !value.is_empty()),
),
None => (
None,
body_json
.and_then(extract_error_message)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned),
),
}
}
pub fn parse_request_candidate_report_context(
report_context: Option<&Value>,
) -> Option<SchedulerRequestCandidateReportContext> {
let report_context = report_context?;
let retry_index = report_context
.get("retry_index")
.and_then(Value::as_u64)
.unwrap_or_default();
Some(SchedulerRequestCandidateReportContext {
request_id: string_field(report_context, "request_id"),
candidate_id: string_field(report_context, "candidate_id"),
user_id: string_field(report_context, "user_id"),
api_key_id: string_field(report_context, "api_key_id"),
candidate_index: report_context
.get("candidate_index")
.and_then(Value::as_u64)
.and_then(|value| u32::try_from(value).ok()),
retry_index: u32::try_from(retry_index).unwrap_or(u32::MAX),
provider_id: string_field(report_context, "provider_id"),
endpoint_id: string_field(report_context, "endpoint_id"),
key_id: string_field(report_context, "key_id"),
client_api_format: string_field(report_context, "client_api_format"),
provider_api_format: string_field(report_context, "provider_api_format"),
})
}
pub fn resolve_report_request_candidate_slot(
existing_candidates: &[StoredRequestCandidate],
metadata: SchedulerRequestCandidateReportContext,
now_unix_secs: u64,
generated_candidate_id: String,
) -> Option<SchedulerResolvedReportRequestCandidateSlot> {
let request_id = metadata.request_id.clone()?;
let matched_candidate = match_existing_report_candidate(existing_candidates, &metadata);
let synthesized_extra_data = build_report_candidate_extra_data(&metadata);
let created_at_unix_secs = matched_candidate
.as_ref()
.map(|candidate| candidate.created_at_unix_secs)
.unwrap_or(now_unix_secs);
let candidate_index = matched_candidate
.as_ref()
.map(|candidate| candidate.candidate_index)
.or(metadata.candidate_index)
.unwrap_or_else(|| next_candidate_index(existing_candidates));
let retry_index = matched_candidate
.as_ref()
.map(|candidate| candidate.retry_index)
.unwrap_or(metadata.retry_index);
Some(SchedulerResolvedReportRequestCandidateSlot {
id: matched_candidate
.as_ref()
.map(|candidate| candidate.id.clone())
.or(metadata.candidate_id)
.unwrap_or(generated_candidate_id),
request_id,
user_id: matched_candidate
.as_ref()
.and_then(|candidate| candidate.user_id.clone())
.or(metadata.user_id),
api_key_id: matched_candidate
.as_ref()
.and_then(|candidate| candidate.api_key_id.clone())
.or(metadata.api_key_id),
candidate_index,
retry_index,
provider_id: matched_candidate
.as_ref()
.and_then(|candidate| candidate.provider_id.clone())
.or(metadata.provider_id),
endpoint_id: matched_candidate
.as_ref()
.and_then(|candidate| candidate.endpoint_id.clone())
.or(metadata.endpoint_id),
key_id: matched_candidate
.as_ref()
.and_then(|candidate| candidate.key_id.clone())
.or(metadata.key_id),
extra_data: matched_candidate
.as_ref()
.and_then(|candidate| candidate.extra_data.clone())
.or(synthesized_extra_data),
created_at_unix_secs,
started_at_unix_secs: matched_candidate
.as_ref()
.and_then(|candidate| candidate.started_at_unix_secs),
finished_at_unix_secs: matched_candidate
.as_ref()
.and_then(|candidate| candidate.finished_at_unix_secs),
})
}
pub fn build_execution_request_candidate_seed(
plan: &ExecutionPlan,
report_context: Option<&Value>,
started_at_unix_secs: u64,
generated_candidate_id: String,
) -> SchedulerExecutionRequestCandidateSeed {
let mut context = report_context
.and_then(Value::as_object)
.cloned()
.unwrap_or_default();
let request_id = string_field(&Value::Object(context.clone()), "request_id")
.unwrap_or_else(|| plan.request_id.clone());
let candidate_index = context
.get("candidate_index")
.and_then(Value::as_u64)
.and_then(|value| u32::try_from(value).ok())
.unwrap_or(0);
let retry_index = context
.get("retry_index")
.and_then(Value::as_u64)
.and_then(|value| u32::try_from(value).ok())
.unwrap_or(0);
let candidate_id = string_field(&Value::Object(context.clone()), "candidate_id")
.unwrap_or(generated_candidate_id);
let user_id = string_field(&Value::Object(context.clone()), "user_id");
let api_key_id = string_field(&Value::Object(context.clone()), "api_key_id");
context.insert("request_id".to_string(), Value::String(request_id.clone()));
context.insert(
"candidate_id".to_string(),
Value::String(candidate_id.clone()),
);
context.insert(
"candidate_index".to_string(),
Value::Number(candidate_index.into()),
);
context.insert(
"provider_id".to_string(),
Value::String(plan.provider_id.clone()),
);
context.insert(
"endpoint_id".to_string(),
Value::String(plan.endpoint_id.clone()),
);
context.insert("key_id".to_string(), Value::String(plan.key_id.clone()));
SchedulerExecutionRequestCandidateSeed {
upsert_record: UpsertRequestCandidateRecord {
id: candidate_id,
request_id,
user_id,
api_key_id,
username: None,
api_key_name: None,
candidate_index,
retry_index,
provider_id: Some(plan.provider_id.clone()),
endpoint_id: Some(plan.endpoint_id.clone()),
key_id: Some(plan.key_id.clone()),
status: RequestCandidateStatus::Pending,
skip_reason: None,
is_cached: Some(false),
status_code: None,
error_type: None,
error_message: None,
latency_ms: None,
concurrent_requests: None,
extra_data: None,
required_capabilities: None,
created_at_unix_secs: Some(started_at_unix_secs),
started_at_unix_secs: Some(started_at_unix_secs),
finished_at_unix_secs: None,
},
report_context: Value::Object(context),
}
}
pub fn build_local_request_candidate_status_record(
plan: &ExecutionPlan,
report_context: Option<&Value>,
status: RequestCandidateStatus,
status_code: Option<u16>,
error_type: Option<String>,
error_message: Option<String>,
latency_ms: Option<u64>,
started_at_unix_secs: Option<u64>,
finished_at_unix_secs: Option<u64>,
) -> Option<UpsertRequestCandidateRecord> {
let candidate_id = plan
.candidate_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())?;
let metadata = parse_request_candidate_report_context(report_context)?;
let candidate_index = metadata.candidate_index?;
Some(UpsertRequestCandidateRecord {
id: candidate_id.to_string(),
request_id: plan.request_id.clone(),
user_id: metadata.user_id,
api_key_id: metadata.api_key_id,
username: None,
api_key_name: None,
candidate_index,
retry_index: metadata.retry_index,
provider_id: Some(plan.provider_id.clone()),
endpoint_id: Some(plan.endpoint_id.clone()),
key_id: Some(plan.key_id.clone()),
status,
skip_reason: None,
is_cached: None,
status_code,
error_type,
error_message,
latency_ms,
concurrent_requests: None,
extra_data: None,
required_capabilities: None,
created_at_unix_secs: None,
started_at_unix_secs,
finished_at_unix_secs,
})
}
pub fn build_report_request_candidate_status_record(
slot: SchedulerResolvedReportRequestCandidateSlot,
status: RequestCandidateStatus,
status_code: Option<u16>,
error_type: Option<String>,
error_message: Option<String>,
latency_ms: Option<u64>,
started_at_unix_secs: Option<u64>,
finished_at_unix_secs: Option<u64>,
now_unix_secs: u64,
) -> UpsertRequestCandidateRecord {
let terminal_unix_secs = finished_at_unix_secs.unwrap_or(now_unix_secs);
let started_at_unix_secs = started_at_unix_secs
.or(slot.started_at_unix_secs)
.or_else(|| status.is_attempted(None).then_some(terminal_unix_secs));
let finished_at_unix_secs = finished_at_unix_secs
.or(slot.finished_at_unix_secs)
.or_else(|| is_terminal_candidate_status(status).then_some(terminal_unix_secs));
UpsertRequestCandidateRecord {
id: slot.id,
request_id: slot.request_id,
user_id: slot.user_id,
api_key_id: slot.api_key_id,
username: None,
api_key_name: None,
candidate_index: slot.candidate_index,
retry_index: slot.retry_index,
provider_id: slot.provider_id,
endpoint_id: slot.endpoint_id,
key_id: slot.key_id,
status,
skip_reason: None,
is_cached: None,
status_code,
error_type,
error_message,
latency_ms,
concurrent_requests: None,
extra_data: slot.extra_data,
required_capabilities: None,
created_at_unix_secs: Some(slot.created_at_unix_secs),
started_at_unix_secs,
finished_at_unix_secs,
}
}
pub fn finalize_execution_request_candidate_report_context(
report_context: Value,
candidate_id: &str,
) -> Value {
let mut context = report_context.as_object().cloned().unwrap_or_default();
let candidate_id = candidate_id.trim();
if !candidate_id.is_empty() {
context.insert(
"candidate_id".to_string(),
Value::String(candidate_id.to_string()),
);
}
Value::Object(context)
}
pub fn is_terminal_candidate_status(status: RequestCandidateStatus) -> bool {
matches!(
status,
RequestCandidateStatus::Unused
| RequestCandidateStatus::Success
| RequestCandidateStatus::Failed
| RequestCandidateStatus::Cancelled
| RequestCandidateStatus::Skipped
)
}
fn extract_error_message(body_json: &Value) -> Option<&str> {
body_json
.get("error")
.and_then(|error| {
error
.get("message")
.and_then(Value::as_str)
.or_else(|| error.as_str())
})
.or_else(|| body_json.get("message").and_then(Value::as_str))
}
fn string_field(value: &Value, key: &str) -> Option<String> {
value
.get(key)
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
fn match_existing_report_candidate<'a>(
candidates: &'a [StoredRequestCandidate],
metadata: &SchedulerRequestCandidateReportContext,
) -> Option<&'a StoredRequestCandidate> {
if let Some(candidate_id) = metadata.candidate_id.as_deref() {
if let Some(candidate) = candidates
.iter()
.find(|candidate| candidate.id == candidate_id)
{
return Some(candidate);
}
}
if let Some(candidate_index) = metadata.candidate_index {
if let Some(candidate) = candidates.iter().find(|candidate| {
candidate.candidate_index == candidate_index
&& candidate.retry_index == metadata.retry_index
}) {
return Some(candidate);
}
}
candidates
.iter()
.filter(|candidate| {
candidate.provider_id.as_deref() == metadata.provider_id.as_deref()
&& candidate.endpoint_id.as_deref() == metadata.endpoint_id.as_deref()
&& candidate.key_id.as_deref() == metadata.key_id.as_deref()
})
.max_by_key(|candidate| {
(
candidate.retry_index,
candidate.candidate_index,
candidate.created_at_unix_secs,
)
})
}
fn next_candidate_index(candidates: &[StoredRequestCandidate]) -> u32 {
candidates
.iter()
.map(|candidate| candidate.candidate_index)
.max()
.map(|value| value.saturating_add(1))
.unwrap_or_default()
}
fn build_report_candidate_extra_data(
metadata: &SchedulerRequestCandidateReportContext,
) -> Option<Value> {
let mut extra_data = serde_json::Map::new();
extra_data.insert("gateway_execution_runtime".to_string(), Value::Bool(true));
extra_data.insert("phase".to_string(), Value::String("3c_trial".to_string()));
if let Some(client_api_format) = metadata.client_api_format.clone() {
extra_data.insert(
"client_api_format".to_string(),
Value::String(client_api_format),
);
}
if let Some(provider_api_format) = metadata.provider_api_format.clone() {
extra_data.insert(
"provider_api_format".to_string(),
Value::String(provider_api_format),
);
}
(!extra_data.is_empty()).then_some(Value::Object(extra_data))
}
#[cfg(test)]
mod tests {
use aether_contracts::{
ExecutionError, ExecutionErrorKind, ExecutionPhase, ExecutionPlan, RequestBody,
};
use aether_data::repository::candidates::{RequestCandidateStatus, StoredRequestCandidate};
use serde_json::{json, Value};
use super::{
build_execution_request_candidate_seed, build_local_request_candidate_status_record,
build_report_request_candidate_status_record, execution_error_details,
finalize_execution_request_candidate_report_context,
parse_request_candidate_report_context, resolve_report_request_candidate_slot,
SchedulerResolvedReportRequestCandidateSlot,
};
fn sample_candidate(
id: &str,
candidate_index: u32,
retry_index: u32,
) -> StoredRequestCandidate {
StoredRequestCandidate::new(
id.to_string(),
"req-1".to_string(),
Some("user-1".to_string()),
Some("key-1".to_string()),
None,
None,
candidate_index as i32,
retry_index as i32,
Some("provider-1".to_string()),
Some("endpoint-1".to_string()),
Some("catalog-key-1".to_string()),
RequestCandidateStatus::Pending,
None,
false,
None,
None,
None,
None,
None,
None,
None,
100,
Some(110),
None,
)
.expect("candidate should build")
}
fn sample_plan() -> ExecutionPlan {
ExecutionPlan {
request_id: "req-1".to_string(),
candidate_id: None,
provider_name: Some("openai".to_string()),
provider_id: "provider-1".to_string(),
endpoint_id: "endpoint-1".to_string(),
key_id: "key-1".to_string(),
method: "POST".to_string(),
url: "https://example.com/v1/chat/completions".to_string(),
headers: Default::default(),
content_type: Some("application/json".to_string()),
content_encoding: None,
body: RequestBody::from_json(json!({"model": "gpt-5"})),
stream: false,
client_api_format: "openai:chat".to_string(),
provider_api_format: "openai:chat".to_string(),
model_name: Some("gpt-5".to_string()),
proxy: None,
tls_profile: None,
timeouts: None,
}
}
#[test]
fn parses_report_context_and_resolves_existing_candidate_slot() {
let metadata = parse_request_candidate_report_context(Some(&json!({
"request_id": "req-1",
"candidate_index": 1,
"retry_index": 2,
"provider_id": "provider-1",
"endpoint_id": "endpoint-1",
"key_id": "catalog-key-1",
"client_api_format": "openai:chat"
})))
.expect("metadata");
let slot = resolve_report_request_candidate_slot(
&[sample_candidate("cand-1", 1, 2)],
metadata,
123,
"generated-1".to_string(),
)
.expect("slot");
assert_eq!(slot.id, "cand-1");
assert_eq!(slot.candidate_index, 1);
assert_eq!(slot.retry_index, 2);
assert_eq!(slot.request_id, "req-1");
}
#[test]
fn resolves_error_details_from_execution_error_or_body_json() {
let error = ExecutionError {
kind: ExecutionErrorKind::Upstream5xx,
phase: ExecutionPhase::FirstByte,
message: " upstream failed ".to_string(),
upstream_status: Some(502),
retryable: true,
failover_recommended: true,
};
assert_eq!(
execution_error_details(Some(&error), None),
(
Some("Upstream5xx".to_string()),
Some("upstream failed".to_string())
)
);
assert_eq!(
execution_error_details(None, Some(&json!({"error": {"message": "bad request"}}))),
(None, Some("bad request".to_string()))
);
}
#[test]
fn builds_execution_request_candidate_seed_and_finalizes_report_context() {
let seed = build_execution_request_candidate_seed(
&sample_plan(),
Some(&json!({
"request_id": "req-override",
"candidate_index": 3,
"retry_index": 2,
"user_id": "user-1",
"api_key_id": "api-key-1",
"client_api_format": "openai:chat"
})),
123,
"generated-1".to_string(),
);
assert_eq!(seed.upsert_record.id, "generated-1");
assert_eq!(seed.upsert_record.request_id, "req-override");
assert_eq!(seed.upsert_record.candidate_index, 3);
assert_eq!(seed.upsert_record.retry_index, 2);
assert_eq!(seed.upsert_record.user_id.as_deref(), Some("user-1"));
assert_eq!(
seed.report_context
.get("provider_id")
.and_then(Value::as_str),
Some("provider-1")
);
let finalized =
finalize_execution_request_candidate_report_context(seed.report_context, "cand-final");
assert_eq!(
finalized.get("candidate_id").and_then(Value::as_str),
Some("cand-final")
);
}
#[test]
fn builds_local_request_candidate_status_record() {
let mut plan = sample_plan();
plan.candidate_id = Some("cand-1".to_string());
let record = build_local_request_candidate_status_record(
&plan,
Some(&json!({
"candidate_index": 1,
"retry_index": 2,
"user_id": "user-1",
"api_key_id": "api-key-1"
})),
RequestCandidateStatus::Failed,
Some(500),
Some("Upstream5xx".to_string()),
Some("boom".to_string()),
Some(42),
Some(100),
Some(101),
)
.expect("record should build");
assert_eq!(record.id, "cand-1");
assert_eq!(record.candidate_index, 1);
assert_eq!(record.retry_index, 2);
assert_eq!(record.user_id.as_deref(), Some("user-1"));
assert_eq!(record.status, RequestCandidateStatus::Failed);
}
#[test]
fn builds_report_request_candidate_status_record_with_terminal_timestamps() {
let record = build_report_request_candidate_status_record(
SchedulerResolvedReportRequestCandidateSlot {
id: "cand-1".to_string(),
request_id: "req-1".to_string(),
user_id: Some("user-1".to_string()),
api_key_id: Some("api-key-1".to_string()),
candidate_index: 1,
retry_index: 0,
provider_id: Some("provider-1".to_string()),
endpoint_id: Some("endpoint-1".to_string()),
key_id: Some("key-1".to_string()),
extra_data: None,
created_at_unix_secs: 10,
started_at_unix_secs: None,
finished_at_unix_secs: None,
},
RequestCandidateStatus::Success,
Some(200),
None,
None,
Some(12),
None,
None,
123,
);
assert_eq!(record.started_at_unix_secs, Some(123));
assert_eq!(record.finished_at_unix_secs, Some(123));
assert_eq!(record.created_at_unix_secs, Some(10));
assert_eq!(record.status, RequestCandidateStatus::Success);
}
}