mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat: 全栈功能增强 - 扩展 provider/pool 管理、完善调度与数据层、重构前端 Pool 页面
后端: - 扩展 pool_admin payloads 和 provider query models,增强 endpoint key 管理 - 完善 scheduler-core 候选排序与请求候选逻辑 - 增强 usage-runtime 写入、provider-transport 网络层与 OAuth 刷新 - 改进 AI pipeline 响应转换与流式处理 - 扩展 global_models/provider_catalog 数据层查询能力 - 增强 video-tasks-core 多 provider 支持 - 新增大量集成测试覆盖 pool/keys/provider_query/frontdoor 前端: - 重构 PoolManagement 页面,拆分状态管理/对话框逻辑到独立模块 - 新增 poolAdvancedDialog/poolSchedulingDialog/poolManagementState/poolMobilePresentation 工具函数及测试 - 改进 Dialog 组件与 provider tabs 显示 部署: - 更新 Rust CI workflow 和 Dockerfile 构建配置 Closes #275 Co-authored-by: AAEE86 <ppk0227@hotmail.com>
This commit is contained in:
@@ -258,16 +258,32 @@ pub fn reorder_candidates_by_scheduler_health(
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CandidateRuntimeSelectabilityInput<'a> {
|
||||
pub candidate: &'a SchedulerMinimalCandidateSelectionCandidate,
|
||||
pub recent_candidates: &'a [StoredRequestCandidate],
|
||||
pub provider_concurrent_limits: &'a BTreeMap<String, usize>,
|
||||
pub provider_key_rpm_states: &'a BTreeMap<String, StoredProviderCatalogKey>,
|
||||
pub now_unix_secs: u64,
|
||||
pub cached_affinity_target: Option<&'a crate::SchedulerAffinityTarget>,
|
||||
pub provider_quota_blocks_requests: bool,
|
||||
pub rpm_reset_at: Option<u64>,
|
||||
}
|
||||
|
||||
pub fn candidate_is_selectable_with_runtime_state(
|
||||
candidate: &SchedulerMinimalCandidateSelectionCandidate,
|
||||
recent_candidates: &[StoredRequestCandidate],
|
||||
provider_concurrent_limits: &BTreeMap<String, usize>,
|
||||
provider_key_rpm_states: &BTreeMap<String, StoredProviderCatalogKey>,
|
||||
now_unix_secs: u64,
|
||||
cached_affinity_target: Option<&crate::SchedulerAffinityTarget>,
|
||||
provider_quota_blocks_requests: bool,
|
||||
rpm_reset_at: Option<u64>,
|
||||
input: CandidateRuntimeSelectabilityInput<'_>,
|
||||
) -> bool {
|
||||
let CandidateRuntimeSelectabilityInput {
|
||||
candidate,
|
||||
recent_candidates,
|
||||
provider_concurrent_limits,
|
||||
provider_key_rpm_states,
|
||||
now_unix_secs,
|
||||
cached_affinity_target,
|
||||
provider_quota_blocks_requests,
|
||||
rpm_reset_at,
|
||||
} = input;
|
||||
|
||||
if provider_quota_blocks_requests {
|
||||
return false;
|
||||
}
|
||||
@@ -375,7 +391,7 @@ mod tests {
|
||||
candidate_is_selectable_with_runtime_state, candidate_supports_required_capability,
|
||||
collect_global_model_names_for_required_capability,
|
||||
collect_selectable_candidates_from_keys, reorder_candidates_by_scheduler_health,
|
||||
SchedulerMinimalCandidateSelectionCandidate,
|
||||
CandidateRuntimeSelectabilityInput, SchedulerMinimalCandidateSelectionCandidate,
|
||||
};
|
||||
use crate::SchedulerAuthConstraints;
|
||||
|
||||
@@ -627,14 +643,16 @@ mod tests {
|
||||
let provider_concurrent_limits = BTreeMap::from([("provider-1".to_string(), 1)]);
|
||||
|
||||
assert!(!candidate_is_selectable_with_runtime_state(
|
||||
&sample_candidate("1", None),
|
||||
&recent_candidates,
|
||||
&provider_concurrent_limits,
|
||||
&BTreeMap::new(),
|
||||
100,
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
CandidateRuntimeSelectabilityInput {
|
||||
candidate: &sample_candidate("1", None),
|
||||
recent_candidates: &recent_candidates,
|
||||
provider_concurrent_limits: &provider_concurrent_limits,
|
||||
provider_key_rpm_states: &BTreeMap::new(),
|
||||
now_unix_secs: 100,
|
||||
cached_affinity_target: None,
|
||||
provider_quota_blocks_requests: false,
|
||||
rpm_reset_at: None,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
@@ -643,24 +661,28 @@ mod tests {
|
||||
let provider_key_rpm_states = BTreeMap::from([("key-1".to_string(), sample_key("1", 0.0))]);
|
||||
|
||||
assert!(!candidate_is_selectable_with_runtime_state(
|
||||
&sample_candidate("1", None),
|
||||
&[],
|
||||
&BTreeMap::new(),
|
||||
&provider_key_rpm_states,
|
||||
100,
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
CandidateRuntimeSelectabilityInput {
|
||||
candidate: &sample_candidate("1", None),
|
||||
recent_candidates: &[],
|
||||
provider_concurrent_limits: &BTreeMap::new(),
|
||||
provider_key_rpm_states: &provider_key_rpm_states,
|
||||
now_unix_secs: 100,
|
||||
cached_affinity_target: None,
|
||||
provider_quota_blocks_requests: false,
|
||||
rpm_reset_at: None,
|
||||
},
|
||||
));
|
||||
assert!(!candidate_is_selectable_with_runtime_state(
|
||||
&sample_candidate("1", None),
|
||||
&[],
|
||||
&BTreeMap::new(),
|
||||
&BTreeMap::new(),
|
||||
100,
|
||||
None,
|
||||
true,
|
||||
None,
|
||||
CandidateRuntimeSelectabilityInput {
|
||||
candidate: &sample_candidate("1", None),
|
||||
recent_candidates: &[],
|
||||
provider_concurrent_limits: &BTreeMap::new(),
|
||||
provider_key_rpm_states: &BTreeMap::new(),
|
||||
now_unix_secs: 100,
|
||||
cached_affinity_target: None,
|
||||
provider_quota_blocks_requests: true,
|
||||
rpm_reset_at: None,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ pub use candidate::{
|
||||
auth_api_key_concurrency_limit_reached, build_minimal_candidate_selection,
|
||||
candidate_is_selectable_with_runtime_state, candidate_supports_required_capability,
|
||||
collect_global_model_names_for_required_capability, collect_selectable_candidates_from_keys,
|
||||
reorder_candidates_by_scheduler_health, SchedulerMinimalCandidateSelectionCandidate,
|
||||
reorder_candidates_by_scheduler_health, CandidateRuntimeSelectabilityInput,
|
||||
SchedulerMinimalCandidateSelectionCandidate,
|
||||
};
|
||||
pub use health::{
|
||||
aggregate_provider_key_health_score, count_recent_active_requests_for_api_key,
|
||||
@@ -40,6 +41,7 @@ pub use request_candidate::{
|
||||
build_report_request_candidate_status_record, execution_error_details,
|
||||
finalize_execution_request_candidate_report_context, is_terminal_candidate_status,
|
||||
parse_request_candidate_report_context, resolve_report_request_candidate_slot,
|
||||
LocalRequestCandidateStatusRecordInput, ReportRequestCandidateStatusRecordInput,
|
||||
SchedulerExecutionRequestCandidateSeed, SchedulerRequestCandidateReportContext,
|
||||
SchedulerResolvedReportRequestCandidateSlot,
|
||||
SchedulerRequestCandidateStatusUpdate, SchedulerResolvedReportRequestCandidateSlot,
|
||||
};
|
||||
|
||||
@@ -90,9 +90,7 @@ pub fn resolve_provider_model_name(
|
||||
}
|
||||
}
|
||||
|
||||
let Some(global_model_mappings) = row.global_model_mappings.as_ref() else {
|
||||
return None;
|
||||
};
|
||||
let global_model_mappings = row.global_model_mappings.as_ref()?;
|
||||
for allowed_model in sorted_allowed_models {
|
||||
for pattern in global_model_mappings {
|
||||
if matches_model_mapping(pattern, &allowed_model) {
|
||||
|
||||
@@ -41,6 +41,31 @@ pub struct SchedulerExecutionRequestCandidateSeed {
|
||||
pub report_context: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SchedulerRequestCandidateStatusUpdate {
|
||||
pub status: RequestCandidateStatus,
|
||||
pub status_code: Option<u16>,
|
||||
pub error_type: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
pub latency_ms: Option<u64>,
|
||||
pub started_at_unix_secs: Option<u64>,
|
||||
pub finished_at_unix_secs: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LocalRequestCandidateStatusRecordInput<'a> {
|
||||
pub plan: &'a ExecutionPlan,
|
||||
pub report_context: Option<&'a Value>,
|
||||
pub status_update: SchedulerRequestCandidateStatusUpdate,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReportRequestCandidateStatusRecordInput {
|
||||
pub slot: SchedulerResolvedReportRequestCandidateSlot,
|
||||
pub status_update: SchedulerRequestCandidateStatusUpdate,
|
||||
pub now_unix_secs: u64,
|
||||
}
|
||||
|
||||
pub fn execution_error_details(
|
||||
error: Option<&ExecutionError>,
|
||||
body_json: Option<&Value>,
|
||||
@@ -231,16 +256,23 @@ pub fn build_execution_request_candidate_seed(
|
||||
}
|
||||
|
||||
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>,
|
||||
input: LocalRequestCandidateStatusRecordInput<'_>,
|
||||
) -> Option<UpsertRequestCandidateRecord> {
|
||||
let LocalRequestCandidateStatusRecordInput {
|
||||
plan,
|
||||
report_context,
|
||||
status_update,
|
||||
} = input;
|
||||
let SchedulerRequestCandidateStatusUpdate {
|
||||
status,
|
||||
status_code,
|
||||
error_type,
|
||||
error_message,
|
||||
latency_ms,
|
||||
started_at_unix_secs,
|
||||
finished_at_unix_secs,
|
||||
} = status_update;
|
||||
|
||||
let candidate_id = plan
|
||||
.candidate_id
|
||||
.as_deref()
|
||||
@@ -278,16 +310,23 @@ pub fn build_local_request_candidate_status_record(
|
||||
}
|
||||
|
||||
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,
|
||||
input: ReportRequestCandidateStatusRecordInput,
|
||||
) -> UpsertRequestCandidateRecord {
|
||||
let ReportRequestCandidateStatusRecordInput {
|
||||
slot,
|
||||
status_update,
|
||||
now_unix_secs,
|
||||
} = input;
|
||||
let SchedulerRequestCandidateStatusUpdate {
|
||||
status,
|
||||
status_code,
|
||||
error_type,
|
||||
error_message,
|
||||
latency_ms,
|
||||
started_at_unix_secs,
|
||||
finished_at_unix_secs,
|
||||
} = status_update;
|
||||
|
||||
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)
|
||||
@@ -454,7 +493,8 @@ mod tests {
|
||||
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,
|
||||
LocalRequestCandidateStatusRecordInput, ReportRequestCandidateStatusRecordInput,
|
||||
SchedulerRequestCandidateStatusUpdate, SchedulerResolvedReportRequestCandidateSlot,
|
||||
};
|
||||
|
||||
fn sample_candidate(
|
||||
@@ -605,23 +645,26 @@ mod tests {
|
||||
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");
|
||||
let record =
|
||||
build_local_request_candidate_status_record(LocalRequestCandidateStatusRecordInput {
|
||||
plan: &plan,
|
||||
report_context: Some(&json!({
|
||||
"candidate_index": 1,
|
||||
"retry_index": 2,
|
||||
"user_id": "user-1",
|
||||
"api_key_id": "api-key-1"
|
||||
})),
|
||||
status_update: SchedulerRequestCandidateStatusUpdate {
|
||||
status: RequestCandidateStatus::Failed,
|
||||
status_code: Some(500),
|
||||
error_type: Some("Upstream5xx".to_string()),
|
||||
error_message: Some("boom".to_string()),
|
||||
latency_ms: Some(42),
|
||||
started_at_unix_secs: Some(100),
|
||||
finished_at_unix_secs: Some(101),
|
||||
},
|
||||
})
|
||||
.expect("record should build");
|
||||
|
||||
assert_eq!(record.id, "cand-1");
|
||||
assert_eq!(record.candidate_index, 1);
|
||||
@@ -632,31 +675,34 @@ mod tests {
|
||||
|
||||
#[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,
|
||||
);
|
||||
let record =
|
||||
build_report_request_candidate_status_record(ReportRequestCandidateStatusRecordInput {
|
||||
slot: 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,
|
||||
},
|
||||
status_update: SchedulerRequestCandidateStatusUpdate {
|
||||
status: RequestCandidateStatus::Success,
|
||||
status_code: Some(200),
|
||||
error_type: None,
|
||||
error_message: None,
|
||||
latency_ms: Some(12),
|
||||
started_at_unix_secs: None,
|
||||
finished_at_unix_secs: None,
|
||||
},
|
||||
now_unix_secs: 123,
|
||||
});
|
||||
|
||||
assert_eq!(record.started_at_unix_secs, Some(123));
|
||||
assert_eq!(record.finished_at_unix_secs, Some(123));
|
||||
|
||||
Reference in New Issue
Block a user