Hide format_conversion_disabled skips when exact-format candidate shares the same key

Also switch failover test to surface an auth failure instead of a 502 upstream error.
This commit is contained in:
fawney19
2026-04-17 18:24:36 +08:00
parent 7eae1f90f6
commit cb647d95f9
3 changed files with 85 additions and 5 deletions
@@ -1196,6 +1196,62 @@ mod tests {
assert_eq!(skipped[0].skip_reason, "format_conversion_disabled");
}
#[tokio::test]
async fn realtime_gate_hides_cross_format_disablement_when_same_key_has_exact_endpoint() {
let provider_catalog = InMemoryProviderCatalogReadRepository::seed(
vec![sample_provider_with_options("provider-shared", false, 0)],
vec![
sample_endpoint_for_provider("provider-shared", "endpoint-exact", "openai:chat"),
sample_endpoint_for_provider("provider-shared", "endpoint-cross", "claude:chat"),
],
vec![sample_key_for_provider_with_options(
"provider-shared",
"key-shared",
"",
true,
Some(json!(["openai:chat", "claude:chat"])),
None,
)],
);
let data_state = GatewayDataState::with_provider_transport_reader_for_tests(
std::sync::Arc::new(provider_catalog),
"development-key",
);
let state = AppState::new()
.expect("state should build")
.with_data_state_for_tests(data_state);
let (ranked, skipped) = filter_and_rank_local_execution_candidates(
PlannerAppState::new(&state),
vec![
sample_priority_candidate(
"provider-shared",
"endpoint-exact",
"key-shared",
"openai:chat",
Some(0),
0,
),
sample_priority_candidate(
"provider-shared",
"endpoint-cross",
"key-shared",
"claude:chat",
Some(0),
0,
),
],
"openai:chat",
"gpt-4.1",
None,
)
.await;
assert_eq!(ranked.len(), 1);
assert_eq!(ranked[0].candidate.endpoint_id, "endpoint-exact");
assert!(skipped.is_empty());
}
#[tokio::test]
async fn realtime_gate_allows_cross_format_candidates_when_endpoint_acceptance_is_enabled() {
let mut endpoint_cross =
@@ -1,6 +1,7 @@
use tracing::warn;
use aether_scheduler_core::SchedulerMinimalCandidateSelectionCandidate;
use std::collections::BTreeSet;
use crate::ai_pipeline::{GatewayProviderTransportSnapshot, PlannerAppState};
@@ -92,6 +93,7 @@ where
{
let mut selectable = Vec::with_capacity(candidates.len());
let mut skipped = Vec::new();
let normalized_client_api_format = client_api_format.trim().to_ascii_lowercase();
for candidate in candidates {
let Some(transport) = read_candidate_transport_snapshot(state, &candidate).await else {
@@ -118,6 +120,28 @@ where
}
}
let exact_selectable_keys = selectable
.iter()
.filter(|candidate| {
candidate
.provider_api_format
.eq_ignore_ascii_case(normalized_client_api_format.as_str())
})
.map(|candidate| {
(
candidate.candidate.provider_id.clone(),
candidate.candidate.key_id.clone(),
)
})
.collect::<BTreeSet<_>>();
skipped.retain(|candidate| {
candidate.skip_reason != "format_conversion_disabled"
|| !exact_selectable_keys.contains(&(
candidate.candidate.provider_id.clone(),
candidate.candidate.key_id.clone(),
))
});
let ranked = rank_eligible_local_execution_candidates(
state,
selectable,