feat(gateway): harden failover and payload handling

Retry pre-response transport failures across candidates with an explicit stop policy, and propagate end-to-end timing into usage records and UI diagnostics.

Remove legacy body, import, cookie, PII, and tunnel replay caps while preserving optional operator-configured gateway limits.
This commit is contained in:
elky
2026-07-30 01:03:27 +08:00
parent a97acc07fc
commit a04673a90d
80 changed files with 3640 additions and 1236 deletions
@@ -782,82 +782,30 @@ async fn ai_execute_pii_redaction_restores_executed_candidate_session_after_late
}
large_stack_async_test!(
pii_redaction_performance_limits_do_not_forward_unredacted_body_upstream,
pii_redaction_performance_limits_do_not_forward_unredacted_body_upstream_impl
pii_redaction_forwards_text_above_previous_scan_cap_only_after_masking,
pii_redaction_forwards_text_above_previous_scan_cap_only_after_masking_impl
);
async fn pii_redaction_performance_limits_do_not_forward_unredacted_body_upstream_impl() {
let provider_hits = Arc::new(AtomicUsize::new(0));
let provider_hits_clone = Arc::clone(&provider_hits);
let provider_app = Router::new().route(
"/v1/chat/completions",
any(move |_request: Request| {
let provider_hits_inner = Arc::clone(&provider_hits_clone);
async move {
provider_hits_inner.fetch_add(1, Ordering::SeqCst);
Json(json!({
"id": "unexpected",
"choices": [{"message": {"role": "assistant", "content": "unexpected"}}]
}))
}
}),
async fn pii_redaction_forwards_text_above_previous_scan_cap_only_after_masking_impl() {
let mut request = rich_pii_request();
request["messages"]
.as_array_mut()
.expect("messages should be an array")
.push(json!({
"role": "user",
"content": "x".repeat(2 * 1024 * 1024 + 1),
}));
let (response_json, seen) =
run_sync_redaction_case("pii-redaction-large-request", true, true, "known", request).await;
let provider_body_text = serde_json::to_string(&seen.body).expect("body should serialize");
assert!(!provider_body_text.contains("[email protected]"));
assert!(provider_body_text.contains("<AETHER:EMAIL:"));
assert_eq!(
response_json["choices"][0]["message"]["content"],
"restored [email protected] access_token=accessValueABCDEF1234567890abcdef secret_key=secretValueABCDEF1234567890abcdef"
);
let (provider_url, provider_handle) = start_server(provider_app).await;
let auth_repository =
auth_repository_with_redaction_feature_settings("pii-redaction-limit", true);
let candidate_selection_repository =
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
candidate_row("pii-redaction-limit"),
]));
let request_candidate_repository = Arc::new(InMemoryRequestCandidateRepository::default());
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![provider("pii-redaction-limit", true)],
vec![endpoint("pii-redaction-limit", provider_url)],
vec![key("pii-redaction-limit")],
));
let data_state = crate::data::GatewayDataState::with_auth_candidate_selection_provider_catalog_and_request_candidate_repository_for_tests(
auth_repository,
candidate_selection_repository,
provider_catalog_repository,
Arc::clone(&request_candidate_repository),
DEVELOPMENT_ENCRYPTION_KEY,
)
.with_system_config_values_for_tests(redaction_config(true));
let gateway_state = AppState::new()
.expect("gateway state should build")
.with_data_state_for_tests(data_state);
let gateway = build_router_with_state(gateway_state);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let original = format!("[email protected] {}", "x".repeat(2 * 1024 * 1024));
let response = reqwest::Client::new()
.post(format!("{gateway_url}/v1/chat/completions"))
.header(http::header::CONTENT_TYPE, "application/json")
.header(
http::header::AUTHORIZATION,
"Bearer sk-client-pii-redaction-limit",
)
.header(TRACE_ID_HEADER, "trace-pii-redaction-limit")
.body(
json!({
"model": "gpt-5",
"messages": [{"role": "user", "content": original}]
})
.to_string(),
)
.send()
.await
.expect("request should complete");
let status = response.status();
let response_text = response.text().await.expect("body should read");
assert_eq!(status, StatusCode::PAYLOAD_TOO_LARGE, "{response_text}");
assert!(response_text.contains("scanned text limit exceeded"));
assert!(!response_text.contains("[email protected]"));
assert_eq!(provider_hits.load(Ordering::SeqCst), 0);
gateway_handle.abort();
provider_handle.abort();
}
large_stack_async_test!(