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

View File

@@ -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 =

View File

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

View File

@@ -675,7 +675,7 @@ async fn gateway_surfaces_local_execution_runtime_miss_reason_when_all_openai_ch
}
#[tokio::test]
async fn gateway_retries_next_local_openai_chat_sync_candidate_with_local_failover_only() {
async fn gateway_retries_next_local_openai_chat_sync_candidate_after_auth_failure() {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeSyncRequest {
trace_id: String,
@@ -951,14 +951,14 @@ async fn gateway_retries_next_local_openai_chat_sync_candidate_with_local_failov
if attempt == 1 {
return Json(json!({
"request_id": "trace-openai-chat-local-failover-123",
"status_code": 502,
"status_code": 401,
"headers": {
"content-type": "application/json"
},
"body": {
"json_body": {
"error": {
"message": "primary unavailable"
"message": "invalid auth token"
}
}
},
@@ -1135,10 +1135,10 @@ async fn gateway_retries_next_local_openai_chat_sync_candidate_with_local_failov
assert_eq!(stored_candidates.len(), 2);
assert_eq!(stored_candidates[0].candidate_index, 0);
assert_eq!(stored_candidates[0].status, RequestCandidateStatus::Failed);
assert_eq!(stored_candidates[0].status_code, Some(502));
assert_eq!(stored_candidates[0].status_code, Some(401));
assert_eq!(
stored_candidates[0].error_message.as_deref(),
Some("primary unavailable")
Some("invalid auth token")
);
assert_eq!(stored_candidates[1].candidate_index, 1);
assert_eq!(stored_candidates[1].status, RequestCandidateStatus::Success);