Unify candidate ranking pipeline

This commit is contained in:
fawney19
2026-04-27 12:34:03 +08:00
parent 9b866a6d17
commit 3b542434a2
46 changed files with 3635 additions and 1914 deletions

View File

@@ -476,6 +476,23 @@ async fn gateway_executes_openai_chat_sync_via_local_cross_format_gemini_candida
}
}
fn sample_disabled_conversion_candidate_row() -> StoredMinimalCandidateSelectionRow {
let mut row = sample_candidate_row();
row.provider_id = "provider-openai-chat-gemini-local-disabled".to_string();
row.provider_priority = 20;
row.endpoint_id = "endpoint-openai-chat-gemini-local-disabled".to_string();
row.key_id = "key-openai-chat-gemini-local-disabled".to_string();
row.model_id = "model-openai-chat-gemini-local-disabled".to_string();
row.global_model_id = "global-model-openai-chat-gemini-local-disabled".to_string();
row.model_provider_model_name = "gemini-2.5-flash-upstream".to_string();
row.model_provider_model_mappings = Some(vec![StoredProviderModelMapping {
name: "gemini-2.5-flash-upstream".to_string(),
priority: 2,
api_formats: Some(vec!["gemini:chat".to_string()]),
}]);
row
}
fn sample_provider_catalog_provider() -> StoredProviderCatalogProvider {
StoredProviderCatalogProvider::new(
"provider-openai-chat-gemini-local-1".to_string(),
@@ -497,6 +514,27 @@ async fn gateway_executes_openai_chat_sync_via_local_cross_format_gemini_candida
)
}
fn sample_disabled_conversion_provider_catalog_provider() -> StoredProviderCatalogProvider {
StoredProviderCatalogProvider::new(
"provider-openai-chat-gemini-local-disabled".to_string(),
"gemini".to_string(),
Some("https://example.com".to_string()),
"custom".to_string(),
)
.expect("provider should build")
.with_transport_fields(
true,
false,
false,
None,
Some(2),
None,
Some(20.0),
None,
None,
)
}
fn sample_provider_catalog_endpoint() -> StoredProviderCatalogEndpoint {
StoredProviderCatalogEndpoint::new(
"endpoint-openai-chat-gemini-local-1".to_string(),
@@ -522,6 +560,29 @@ async fn gateway_executes_openai_chat_sync_via_local_cross_format_gemini_candida
.expect("endpoint transport should build")
}
fn sample_disabled_conversion_provider_catalog_endpoint() -> StoredProviderCatalogEndpoint {
StoredProviderCatalogEndpoint::new(
"endpoint-openai-chat-gemini-local-disabled".to_string(),
"provider-openai-chat-gemini-local-disabled".to_string(),
"gemini:chat".to_string(),
Some("gemini".to_string()),
Some("chat".to_string()),
true,
)
.expect("endpoint should build")
.with_transport_fields(
"https://generativelanguage.googleapis.com".to_string(),
None,
None,
Some(2),
Some("/custom/v1beta/models/gemini-2.5-flash-upstream:generateContent".to_string()),
None,
None,
None,
)
.expect("endpoint transport should build")
}
fn sample_provider_catalog_key() -> StoredProviderCatalogKey {
StoredProviderCatalogKey::new(
"key-openai-chat-gemini-local-1".to_string(),
@@ -550,6 +611,34 @@ async fn gateway_executes_openai_chat_sync_via_local_cross_format_gemini_candida
.expect("key transport should build")
}
fn sample_disabled_conversion_provider_catalog_key() -> StoredProviderCatalogKey {
StoredProviderCatalogKey::new(
"key-openai-chat-gemini-local-disabled".to_string(),
"provider-openai-chat-gemini-local-disabled".to_string(),
"prod".to_string(),
"api_key".to_string(),
None,
true,
)
.expect("key should build")
.with_transport_fields(
Some(serde_json::json!(["gemini:chat"])),
encrypt_python_fernet_plaintext(
DEVELOPMENT_ENCRYPTION_KEY,
"sk-upstream-openai-chat-gemini-disabled",
)
.expect("api key should encrypt"),
None,
None,
Some(serde_json::json!({"gemini:chat": 2})),
None,
None,
None,
None,
)
.expect("key transport should build")
}
let seen_execution_runtime = Arc::new(Mutex::new(None::<SeenExecutionRuntimeSyncRequest>));
let seen_execution_runtime_clone = Arc::clone(&seen_execution_runtime);
let seen_report = Arc::new(Mutex::new(false));
@@ -730,11 +819,21 @@ async fn gateway_executes_openai_chat_sync_via_local_cross_format_gemini_candida
let candidate_selection_repository =
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
sample_candidate_row(),
sample_disabled_conversion_candidate_row(),
]));
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![sample_provider_catalog_provider()],
vec![sample_provider_catalog_endpoint()],
vec![sample_provider_catalog_key()],
vec![
sample_provider_catalog_provider(),
sample_disabled_conversion_provider_catalog_provider(),
],
vec![
sample_provider_catalog_endpoint(),
sample_disabled_conversion_provider_catalog_endpoint(),
],
vec![
sample_provider_catalog_key(),
sample_disabled_conversion_provider_catalog_key(),
],
));
let (upstream_url, upstream_handle) = start_server(upstream).await;
@@ -833,8 +932,31 @@ async fn gateway_executes_openai_chat_sync_via_local_cross_format_gemini_candida
.list_by_request_id("trace-openai-chat-gemini-local-123")
.await
.expect("request candidate trace should read");
assert_eq!(stored_candidates.len(), 1);
assert_eq!(stored_candidates[0].status, RequestCandidateStatus::Success);
assert_eq!(stored_candidates.len(), 2);
assert_eq!(
stored_candidates
.iter()
.filter(|candidate| candidate.status == RequestCandidateStatus::Success)
.count(),
1
);
let skipped_candidate = stored_candidates
.iter()
.find(|candidate| candidate.status == RequestCandidateStatus::Skipped)
.expect("disabled conversion candidate should be persisted as skipped");
assert_eq!(
skipped_candidate.skip_reason.as_deref(),
Some("format_conversion_disabled")
);
let extra_data = skipped_candidate
.extra_data
.as_ref()
.expect("skipped cross-format candidate extra_data should exist");
assert_eq!(extra_data["execution_strategy"], "local_cross_format");
assert_eq!(
extra_data["transport_diagnostics"]["request_pair"]["conversion_enabled"],
false
);
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
assert!(

View File

@@ -423,6 +423,7 @@ fn ai_pipeline_planner_separates_local_candidate_eligibility_from_affinity_ranki
for pattern in [
"mod candidate_affinity;",
"mod candidate_eligibility;",
"mod candidate_resolution;",
"mod candidate_preparation;",
] {
assert!(
@@ -431,19 +432,30 @@ fn ai_pipeline_planner_separates_local_candidate_eligibility_from_affinity_ranki
);
}
let candidate_eligibility =
read_workspace_file("apps/aether-gateway/src/ai_pipeline/planner/candidate_eligibility.rs");
let candidate_resolution =
read_workspace_file("apps/aether-gateway/src/ai_pipeline/planner/candidate_resolution.rs");
for pattern in [
"pub(crate) async fn filter_and_rank_local_execution_candidates(",
"pub(crate) async fn filter_and_rank_local_execution_candidates_without_transport_pair_gate(",
"pub(crate) async fn read_candidate_transport_snapshot(",
] {
assert!(
candidate_eligibility.contains(pattern),
"planner/candidate_eligibility.rs should own {pattern}"
candidate_resolution.contains(pattern),
"planner/candidate_resolution.rs should own {pattern}"
);
}
let candidate_eligibility =
read_workspace_file("apps/aether-gateway/src/ai_pipeline/planner/candidate_eligibility.rs");
assert!(
candidate_eligibility.contains("pub(crate) use super::candidate_resolution::*;"),
"planner/candidate_eligibility.rs should remain a compatibility shim"
);
assert!(
!candidate_eligibility.contains("async fn filter_and_rank_local_execution_candidates("),
"planner/candidate_eligibility.rs should not keep resolution implementation"
);
let candidate_affinity =
read_workspace_file("apps/aether-gateway/src/ai_pipeline/planner/candidate_affinity.rs");
assert!(
@@ -1233,11 +1245,11 @@ fn ai_pipeline_specialized_files_attempts_consume_eligible_local_candidates_with
assert!(
specialized_files_support
.contains("filter_and_rank_local_execution_candidates_without_transport_pair_gate("),
"specialized files support should source runtime gating from candidate_eligibility"
"specialized files support should source runtime gating from candidate_resolution"
);
assert!(
!specialized_files_support.contains("rank_local_execution_candidates("),
"specialized files support should not bypass candidate_eligibility with raw affinity ranking"
"specialized files support should not bypass candidate_resolution with raw affinity ranking"
);
let specialized_files_decision = read_workspace_file(

View File

@@ -254,12 +254,20 @@ fn scheduler_candidate_runtime_paths_depend_on_scheduler_core_and_state_trait()
"selection.rs should not depend on gateway-local SchedulerAffinityTarget"
);
assert!(
selection.contains("reorder_candidates_by_scheduler_health_in_core"),
"selection.rs should depend on core candidate reorder helper"
selection.contains("enumerate_scheduler_candidates("),
"selection.rs should delegate candidate enumeration"
);
assert!(
selection.contains("collect_selectable_candidates_from_keys"),
"selection.rs should depend on core selectable-candidate collector"
selection.contains("read_candidate_runtime_selection_snapshot("),
"selection.rs should delegate runtime snapshot loading"
);
assert!(
selection.contains("resolve_scheduler_candidate_selectability("),
"selection.rs should delegate selectability resolution"
);
assert!(
selection.contains("rank_scheduler_candidates("),
"selection.rs should delegate final ranking"
);
for pattern in [
"async fn collect_selectable_candidates(",
@@ -282,8 +290,10 @@ fn scheduler_candidate_runtime_paths_depend_on_scheduler_core_and_state_trait()
"read_provider_concurrent_limits(",
"read_provider_key_rpm_states(",
"candidate_is_selectable_with_runtime_state",
"collect_selectable_candidates_from_keys",
"auth_api_key_concurrency_limit_reached",
"build_provider_concurrent_limit_map(",
"reorder_candidates_by_scheduler_health",
] {
assert!(
!selection.contains(pattern),
@@ -558,8 +568,8 @@ fn scheduler_candidate_runtime_paths_depend_on_scheduler_core_and_state_trait()
let planner_candidate_affinity =
read_workspace_file("apps/aether-gateway/src/ai_pipeline/planner/candidate_affinity.rs");
assert!(
planner_candidate_affinity
.contains("aether_scheduler_core::SchedulerMinimalCandidateSelectionCandidate"),
planner_candidate_affinity.contains("use aether_scheduler_core::{")
&& planner_candidate_affinity.contains("SchedulerMinimalCandidateSelectionCandidate"),
"planner/candidate_affinity.rs should depend directly on core minimal candidate DTO"
);
assert!(