mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 18:07:47 +08:00
fix: align Responses routing and model permissions
This commit is contained in:
@@ -188,9 +188,14 @@ pub(crate) async fn scheduler_ordering_config_for_routing_policy(
|
||||
state: PlannerAppState<'_>,
|
||||
routing_policy: Option<&ResolvedRoutingPolicy>,
|
||||
) -> SchedulerOrderingConfig {
|
||||
let system_config = read_scheduler_ordering_config_or_default(state).await;
|
||||
match routing_policy {
|
||||
Some(policy) => scheduler_ordering_config_from_routing_policy(policy),
|
||||
None => read_scheduler_ordering_config_or_default(state).await,
|
||||
Some(policy) => {
|
||||
let mut config = scheduler_ordering_config_from_routing_policy(policy);
|
||||
config.keep_priority_on_conversion |= system_config.keep_priority_on_conversion;
|
||||
config
|
||||
}
|
||||
None => system_config,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,6 +395,43 @@ mod tests {
|
||||
assert_eq!(overlaid.key_global_priority_for_format, Some(2));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn routing_policy_inherits_global_conversion_priority_override() {
|
||||
let data_state = GatewayDataState::default().with_system_config_values_for_tests([(
|
||||
"keep_priority_on_conversion".to_string(),
|
||||
json!(true),
|
||||
)]);
|
||||
let state = AppState::new()
|
||||
.expect("state should build")
|
||||
.with_data_state_for_tests(data_state);
|
||||
let policy = aether_routing_core::ResolvedRoutingPolicy {
|
||||
group_id: Some("group-1".to_string()),
|
||||
group_version: Some(1),
|
||||
selection_source: "system_default".to_string(),
|
||||
requested_model: "gpt-5.4-mini".to_string(),
|
||||
resolved_model: "gpt-5.4-mini".to_string(),
|
||||
priority_mode: aether_routing_core::RoutingSetPriorityMode::Provider,
|
||||
scheduling_mode: aether_routing_core::RoutingSchedulingMode::FixedOrder,
|
||||
keep_priority_on_conversion: false,
|
||||
ranking_overlay: Default::default(),
|
||||
mutation_plan: Default::default(),
|
||||
pool_policy_overrides: Default::default(),
|
||||
matched_rules: Vec::new(),
|
||||
};
|
||||
|
||||
let ordering = super::scheduler_ordering_config_for_routing_policy(
|
||||
PlannerAppState::new(&state),
|
||||
Some(&policy),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
ordering.scheduling_mode,
|
||||
crate::scheduler::config::SchedulerSchedulingMode::FixedOrder
|
||||
);
|
||||
assert!(ordering.keep_priority_on_conversion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn routing_policy_uses_pool_priority_for_pool_group_global_key_slot() {
|
||||
let mut candidate = sample_candidate("endpoint-1", "representative-key");
|
||||
|
||||
@@ -2540,4 +2540,146 @@ mod tests {
|
||||
vec!["provider-openai-responses-regular"]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fixed_order_prefers_codex_responses_when_conversion_keeps_priority() {
|
||||
let mut codex = standard_candidate_row("provider-codex", "openai:responses", 0);
|
||||
codex.provider_type = "codex".to_string();
|
||||
codex.key_auth_type = "oauth".to_string();
|
||||
codex.global_model_name = "gpt-5.4-mini".to_string();
|
||||
codex.model_provider_model_name = "gpt-5.4-mini".to_string();
|
||||
let mut custom_chat = standard_candidate_row("provider-custom", "openai:chat", 10);
|
||||
custom_chat.global_model_name = "gpt-5.4-mini".to_string();
|
||||
custom_chat.model_provider_model_name = "gpt-5.4-mini".to_string();
|
||||
|
||||
let candidate_repository: Arc<dyn MinimalCandidateSelectionReadRepository> =
|
||||
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed([
|
||||
codex.clone(),
|
||||
custom_chat.clone(),
|
||||
]));
|
||||
let catalog_items = [
|
||||
provider_catalog_for_standard_row(&codex, false),
|
||||
provider_catalog_for_standard_row(&custom_chat, false),
|
||||
];
|
||||
let provider_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||
catalog_items
|
||||
.iter()
|
||||
.map(|(provider, _, _)| provider.clone())
|
||||
.collect(),
|
||||
catalog_items
|
||||
.iter()
|
||||
.map(|(_, endpoint, _)| endpoint.clone())
|
||||
.collect(),
|
||||
catalog_items
|
||||
.iter()
|
||||
.map(|(_, _, key)| key.clone())
|
||||
.collect(),
|
||||
));
|
||||
let data_state =
|
||||
GatewayDataState::with_provider_catalog_and_minimal_candidate_selection_for_tests(
|
||||
provider_repository,
|
||||
candidate_repository,
|
||||
)
|
||||
.with_encryption_key_for_tests("development-key")
|
||||
.with_system_config_values_for_tests([
|
||||
(
|
||||
"scheduling_mode".to_string(),
|
||||
serde_json::json!("fixed_order"),
|
||||
),
|
||||
(
|
||||
"keep_priority_on_conversion".to_string(),
|
||||
serde_json::json!(true),
|
||||
),
|
||||
]);
|
||||
let app = AppState::new()
|
||||
.expect("gateway state should build")
|
||||
.with_data_state_for_tests(data_state);
|
||||
let auth_snapshot = unrestricted_auth_snapshot();
|
||||
let model_directive_policy =
|
||||
crate::system_features::ModelDirectivePolicySnapshot::load(&app).await;
|
||||
let routing_policy = ResolvedRoutingPolicy {
|
||||
group_id: Some("routing-group-codex-first".to_string()),
|
||||
group_version: Some(1),
|
||||
selection_source: "test".to_string(),
|
||||
requested_model: "gpt-5.4-mini".to_string(),
|
||||
resolved_model: "gpt-5.4-mini".to_string(),
|
||||
priority_mode: aether_routing_core::RoutingSetPriorityMode::Provider,
|
||||
scheduling_mode: aether_routing_core::RoutingSchedulingMode::FixedOrder,
|
||||
keep_priority_on_conversion: false,
|
||||
ranking_overlay: Default::default(),
|
||||
mutation_plan: Default::default(),
|
||||
pool_policy_overrides: Default::default(),
|
||||
matched_rules: Vec::new(),
|
||||
};
|
||||
let mut cursor = LocalCandidatePreselectionPageCursor::new(
|
||||
PlannerAppState::new(&app),
|
||||
&model_directive_policy,
|
||||
"openai:chat",
|
||||
"gpt-5.4-mini",
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
&auth_snapshot,
|
||||
Some(&routing_policy),
|
||||
None,
|
||||
None,
|
||||
true,
|
||||
LocalCandidatePreselectionKeyMode::ProviderEndpointKeyModelAndApiFormat,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let first_page = cursor
|
||||
.next_page()
|
||||
.await
|
||||
.expect("preselection should succeed")
|
||||
.expect("Codex and custom candidates should share the priority page");
|
||||
assert!(
|
||||
first_page.skipped_candidates.is_empty(),
|
||||
"priority page unexpectedly skipped candidates: {:?}",
|
||||
first_page
|
||||
.skipped_candidates
|
||||
.iter()
|
||||
.map(|candidate| {
|
||||
(
|
||||
candidate.candidate.provider_id.as_str(),
|
||||
candidate.skip_reason,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
first_page
|
||||
.candidates
|
||||
.iter()
|
||||
.map(|candidate| candidate.provider_id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["provider-custom", "provider-codex"]
|
||||
);
|
||||
let (ranked, skipped) =
|
||||
super::super::candidate_resolution::resolve_and_rank_logical_local_execution_candidates(
|
||||
PlannerAppState::new(&app),
|
||||
first_page.candidates,
|
||||
"openai:chat",
|
||||
Some("gpt-5.4-mini"),
|
||||
Some(&auth_snapshot),
|
||||
None,
|
||||
None,
|
||||
Some(&routing_policy),
|
||||
None,
|
||||
None,
|
||||
aether_ai_serving::AiCandidateResolutionMode::Standard,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(skipped.is_empty());
|
||||
assert_eq!(
|
||||
ranked
|
||||
.iter()
|
||||
.map(|candidate| candidate.candidate.provider_id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["provider-codex", "provider-custom"]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,7 +352,7 @@ fn final_openai_provider_contract_uses_the_mapped_model_for_reasoning() {
|
||||
false,
|
||||
)
|
||||
.is_some());
|
||||
assert!(build_local_openai_responses_request_body(
|
||||
let remapped = build_local_openai_responses_request_body(
|
||||
&alias,
|
||||
"gpt-5.4",
|
||||
false,
|
||||
@@ -364,14 +364,15 @@ fn final_openai_provider_contract_uses_the_mapped_model_for_reasoning() {
|
||||
&http::HeaderMap::new(),
|
||||
false,
|
||||
)
|
||||
.is_none());
|
||||
.expect("explicit reasoning effort should pass through to the mapped model");
|
||||
assert_eq!(remapped["reasoning"]["effort"], "max");
|
||||
|
||||
let minimal = json!({
|
||||
"model": "deployment-alias",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"reasoning_effort": "minimal"
|
||||
});
|
||||
assert!(build_local_openai_chat_request_body(
|
||||
let minimal = build_local_openai_chat_request_body(
|
||||
&minimal,
|
||||
"gpt-5.6-terra",
|
||||
false,
|
||||
@@ -380,7 +381,8 @@ fn final_openai_provider_contract_uses_the_mapped_model_for_reasoning() {
|
||||
&http::HeaderMap::new(),
|
||||
false,
|
||||
)
|
||||
.is_none());
|
||||
.expect("explicit chat reasoning effort should be validated by the upstream");
|
||||
assert_eq!(minimal["reasoning_effort"], "minimal");
|
||||
|
||||
let opaque_mapping = json!({
|
||||
"model": "gpt-5.6-sol-max",
|
||||
@@ -426,7 +428,7 @@ fn final_openai_provider_contract_validates_body_rule_output() {
|
||||
let model_override = json!([
|
||||
{"action":"set","path":"model","value":"gpt-5.4"}
|
||||
]);
|
||||
assert!(build_local_openai_responses_request_body(
|
||||
let provider_request = build_local_openai_responses_request_body(
|
||||
&body,
|
||||
"gpt-5.6-sol",
|
||||
false,
|
||||
@@ -438,7 +440,9 @@ fn final_openai_provider_contract_validates_body_rule_output() {
|
||||
&http::HeaderMap::new(),
|
||||
false,
|
||||
)
|
||||
.is_none());
|
||||
.expect("body rule output should preserve explicit reasoning effort");
|
||||
assert_eq!(provider_request["model"], "gpt-5.4");
|
||||
assert_eq!(provider_request["reasoning"]["effort"], "max");
|
||||
|
||||
let cache_override = json!([
|
||||
{"action":"set","path":"prompt_cache_options.ttl","value":"1h"}
|
||||
|
||||
Reference in New Issue
Block a user