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:
fawney19
2026-04-09 13:51:50 +08:00
parent 4fc95adfb9
commit b0b40c16ff
97 changed files with 5816 additions and 1881 deletions

View File

@@ -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,
},
));
}

View File

@@ -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,
};

View File

@@ -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) {

View File

@@ -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));