fix(ai): harden responses replay and failure diagnostics

This commit is contained in:
elky
2026-07-30 17:19:54 +08:00
parent 1ab4f079c9
commit 050eb77508
29 changed files with 700 additions and 142 deletions
@@ -41,7 +41,8 @@ use super::{
LocalSameFormatProviderSpec,
};
use crate::ai_serving::planner::standard::{
codex_model_capabilities_for_transport, same_format_provider_request_body_failure_extra_data,
codex_model_capabilities_for_transport, openai_provider_request_contract_failure_extra_data,
same_format_provider_request_body_failure_extra_data,
};
pub(crate) fn resolve_same_format_provider_transport_unsupported_reason_for_trace(
@@ -267,33 +268,39 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts(
prepared.mapped_model.as_str(),
source_model,
);
if crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities(
&mut base_provider_request_body,
crate::ai_serving::OpenAiProviderRequestFinalization {
source_api_format: spec.api_format,
provider_api_format: prepared.provider_api_format.as_str(),
provider_type: transport.provider.provider_type.as_str(),
provider_model: prepared.mapped_model.as_str(),
source_model,
body_rules: transport.endpoint.body_rules.as_ref(),
upstream_is_stream: prepared.upstream_is_stream,
require_body_stream_field: request_requires_body_stream_field(
body_json,
prepared.force_body_stream_field,
),
},
codex_model_capabilities.as_ref(),
)
.is_err()
if let Err(violation) =
crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities(
&mut base_provider_request_body,
crate::ai_serving::OpenAiProviderRequestFinalization {
source_api_format: spec.api_format,
provider_api_format: prepared.provider_api_format.as_str(),
provider_type: transport.provider.provider_type.as_str(),
provider_model: prepared.mapped_model.as_str(),
source_model,
body_rules: transport.endpoint.body_rules.as_ref(),
upstream_is_stream: prepared.upstream_is_stream,
require_body_stream_field: request_requires_body_stream_field(
body_json,
prepared.force_body_stream_field,
),
},
codex_model_capabilities.as_ref(),
)
{
mark_skipped_local_same_format_provider_candidate(
mark_skipped_local_same_format_provider_candidate_with_extra_data(
state,
input,
trace_id,
candidate,
attempt.candidate_index,
&attempt.candidate_id,
"provider_request_body_missing",
"provider_request_body_build_failed",
Some(openai_provider_request_contract_failure_extra_data(
&violation,
spec.api_format,
prepared.provider_api_format.as_str(),
"same_format_provider_request_finalization",
)),
)
.await;
return Ok(None);
@@ -23,7 +23,8 @@ use crate::ai_serving::planner::spec_metadata::local_standard_spec_metadata;
use crate::ai_serving::planner::standard::{
apply_codex_openai_special_headers, apply_deepseek_tool_call_thinking_compat,
codex_model_capabilities_for_transport, is_deepseek_provider,
request_body_build_failure_extra_data, request_conversion_failure_extra_data,
openai_provider_request_contract_failure_extra_data, request_body_build_failure_extra_data,
request_conversion_failure_extra_data,
};
use crate::ai_serving::transport::kiro::{
build_kiro_provider_headers, build_kiro_provider_request_body,
@@ -740,24 +741,24 @@ pub(crate) async fn resolve_local_standard_candidate_payload_parts(
prepared_candidate.mapped_model.as_str(),
source_model,
);
if crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities(
&mut provider_request_body,
crate::ai_serving::OpenAiProviderRequestFinalization {
source_api_format: spec_metadata.api_format,
provider_api_format,
provider_type: transport.provider.provider_type.as_str(),
provider_model: prepared_candidate.mapped_model.as_str(),
source_model,
body_rules: transport.endpoint.body_rules.as_ref(),
upstream_is_stream,
require_body_stream_field: request_requires_body_stream_field(
body_json,
force_body_stream_field,
),
},
codex_model_capabilities.as_ref(),
)
.is_err()
if let Err(violation) =
crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities(
&mut provider_request_body,
crate::ai_serving::OpenAiProviderRequestFinalization {
source_api_format: spec_metadata.api_format,
provider_api_format,
provider_type: transport.provider.provider_type.as_str(),
provider_model: prepared_candidate.mapped_model.as_str(),
source_model,
body_rules: transport.endpoint.body_rules.as_ref(),
upstream_is_stream,
require_body_stream_field: request_requires_body_stream_field(
body_json,
force_body_stream_field,
),
},
codex_model_capabilities.as_ref(),
)
{
mark_skipped_local_standard_candidate_with_extra_data(
state,
@@ -767,15 +768,12 @@ pub(crate) async fn resolve_local_standard_candidate_payload_parts(
attempt.candidate_index,
&attempt.candidate_id,
"provider_request_body_build_failed",
request_conversion_failure_extra_data(
body_json,
Some(openai_provider_request_contract_failure_extra_data(
&violation,
spec_metadata.api_format,
provider_api_format,
Some(prepared_candidate.mapped_model.as_str()),
Some(parts.uri.path()),
upstream_is_stream,
"standard_family_request_finalization",
),
)),
)
.await;
return Ok(None);
@@ -65,8 +65,8 @@ pub(crate) use crate::ai_serving::{
normalize_openai_responses_request_to_openai_chat_request, parse_openai_tool_result_content,
};
pub(crate) use aether_ai_serving::{
request_body_build_failure_extra_data, request_conversion_failure_extra_data,
same_format_provider_request_body_failure_extra_data,
openai_provider_request_contract_failure_extra_data, request_body_build_failure_extra_data,
request_conversion_failure_extra_data, same_format_provider_request_body_failure_extra_data,
};
pub(crate) fn build_standard_upstream_url(
@@ -81,6 +81,10 @@ pub(crate) fn build_local_openai_responses_request_body_with_codex_model_capabil
&mut provider_request_body,
provider_api_format,
);
crate::ai_serving::strip_incompatible_openai_responses_reasoning_items(
&mut provider_request_body,
provider_api_format,
);
enforce_provider_body_stream_policy(
&mut provider_request_body,
provider_api_format,
@@ -169,6 +173,10 @@ pub(crate) fn build_cross_format_openai_responses_request_body_with_codex_model_
&mut provider_request_body,
provider_api_format,
);
crate::ai_serving::strip_incompatible_openai_responses_reasoning_items(
&mut provider_request_body,
provider_api_format,
);
enforce_provider_body_stream_policy(
&mut provider_request_body,
provider_api_format,
@@ -152,6 +152,43 @@ fn local_openai_responses_wrapper_preserves_body_order_after_edits() {
assert!(provider_request_body.get("instructions").is_none());
}
#[test]
fn local_openai_responses_wrapper_strips_foreign_reasoning_item_ids() {
let body_json = json!({
"model": "gpt-5.4",
"input": [
{"type": "reasoning", "id": "rs_provider_123", "summary": []},
{
"type": "reasoning",
"id": "item_72d3bd8d367d01977ace23f1",
"summary": []
},
{"type": "message", "role": "user", "content": "continue"}
]
});
let provider_request_body = build_local_openai_responses_request_body(
&body_json,
"gpt-5.4",
false,
false,
"codex",
"openai:responses",
None,
None,
&http::HeaderMap::new(),
false,
)
.expect("local OpenAI Responses body should build");
let input = provider_request_body["input"]
.as_array()
.expect("input array");
assert_eq!(input.len(), 2);
assert_eq!(input[0]["id"], "rs_provider_123");
assert_eq!(input[1]["type"], "message");
}
#[test]
fn local_openai_responses_compact_wrapper_strips_store_for_same_format_requests() {
let body_json = json!({
@@ -29,7 +29,8 @@ use crate::ai_serving::planner::standard::{
apply_deepseek_tool_call_thinking_compat, build_cross_format_openai_chat_request_body,
build_cross_format_openai_chat_upstream_url, build_local_openai_chat_request_body,
build_local_openai_chat_upstream_url, codex_model_capabilities_for_transport,
request_body_build_failure_extra_data, request_conversion_failure_extra_data,
openai_provider_request_contract_failure_extra_data, request_body_build_failure_extra_data,
request_conversion_failure_extra_data,
};
use crate::ai_serving::transport::antigravity::is_antigravity_provider_transport;
use crate::ai_serving::transport::auth::resolve_local_openai_bearer_auth;
@@ -109,7 +110,7 @@ fn finalize_openai_chat_provider_request_body(
original_body: &Value,
transport: &GatewayProviderTransportSnapshot,
mapped_model: &str,
) -> bool {
) -> Option<Value> {
if let Some(mapping) = custom_directive_mapping {
crate::ai_serving::apply_model_directive_mapping_patch(provider_request_body, mapping);
}
@@ -156,7 +157,15 @@ fn finalize_openai_chat_provider_request_body(
},
codex_model_capabilities.as_ref(),
)
.is_ok()
.err()
.map(|violation| {
openai_provider_request_contract_failure_extra_data(
&violation,
"openai:chat",
provider_api_format,
"openai_chat_request_finalization",
)
})
}
#[allow(clippy::too_many_arguments)]
@@ -288,7 +297,7 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
.await;
return Ok(None);
};
if !finalize_openai_chat_provider_request_body(
if let Some(extra_data) = finalize_openai_chat_provider_request_body(
&mut provider_request_body,
model_directive_mapping.as_ref(),
provider_api_format,
@@ -306,11 +315,7 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
candidate_index,
candidate_id,
"provider_request_body_build_failed",
request_body_build_failure_extra_data(
body_json,
"openai:chat",
provider_api_format,
),
Some(extra_data),
)
.await;
return Ok(None);
@@ -501,7 +506,7 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
"openai_chat_payload_body_build",
body_build_started_at.elapsed().as_millis() as u64,
);
if !finalize_openai_chat_provider_request_body(
if let Some(extra_data) = finalize_openai_chat_provider_request_body(
&mut provider_request_body,
model_directive_mapping.as_ref(),
"openai:chat",
@@ -519,11 +524,7 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
candidate_index,
candidate_id,
"provider_request_body_build_failed",
request_body_build_failure_extra_data(
body_json,
"openai:chat",
provider_api_format,
),
Some(extra_data),
)
.await;
return Ok(None);
@@ -813,7 +814,7 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
.await;
return Ok(None);
};
if !finalize_openai_chat_provider_request_body(
if let Some(extra_data) = finalize_openai_chat_provider_request_body(
&mut provider_request_body,
model_directive_mapping.as_ref(),
provider_api_format.as_str(),
@@ -831,15 +832,7 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
candidate_index,
candidate_id,
"provider_request_body_build_failed",
request_conversion_failure_extra_data(
body_json,
"openai:chat",
provider_api_format.as_str(),
Some(prepared_candidate.mapped_model.as_str()),
Some(parts.uri.path()),
upstream_is_stream,
"openai_chat_request_conversion",
),
Some(extra_data),
)
.await;
return Ok(None);
@@ -32,7 +32,8 @@ use crate::ai_serving::planner::standard::{
build_cross_format_openai_responses_upstream_url,
build_local_openai_responses_request_body_with_codex_model_capabilities,
build_local_openai_responses_upstream_url, codex_model_capabilities_for_transport,
request_body_build_failure_extra_data, request_conversion_failure_extra_data,
openai_provider_request_contract_failure_extra_data, request_body_build_failure_extra_data,
request_conversion_failure_extra_data,
};
use crate::ai_serving::transport::antigravity::is_antigravity_provider_transport;
use crate::ai_serving::transport::auth::{
@@ -431,24 +432,24 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
provider_api_format,
Some(body_json),
);
if crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities(
&mut base_provider_request_body,
crate::ai_serving::OpenAiProviderRequestFinalization {
source_api_format: spec_metadata.api_format,
provider_api_format,
provider_type: transport.provider.provider_type.as_str(),
provider_model: mapped_model.as_str(),
source_model,
body_rules: transport.endpoint.body_rules.as_ref(),
upstream_is_stream,
require_body_stream_field: request_requires_body_stream_field(
body_json,
force_body_stream_field,
),
},
codex_model_capabilities.as_ref(),
)
.is_err()
if let Err(violation) =
crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities(
&mut base_provider_request_body,
crate::ai_serving::OpenAiProviderRequestFinalization {
source_api_format: spec_metadata.api_format,
provider_api_format,
provider_type: transport.provider.provider_type.as_str(),
provider_model: mapped_model.as_str(),
source_model,
body_rules: transport.endpoint.body_rules.as_ref(),
upstream_is_stream,
require_body_stream_field: request_requires_body_stream_field(
body_json,
force_body_stream_field,
),
},
codex_model_capabilities.as_ref(),
)
{
mark_skipped_local_openai_responses_candidate_with_extra_data(
state,
@@ -458,15 +459,12 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
candidate_index,
candidate_id,
"provider_request_body_build_failed",
request_conversion_failure_extra_data(
body_json,
Some(openai_provider_request_contract_failure_extra_data(
&violation,
spec_metadata.api_format,
provider_api_format,
Some(mapped_model.as_str()),
Some(parts.uri.path()),
upstream_is_stream,
"openai_responses_request_conversion",
),
"openai_responses_request_finalization",
)),
)
.await;
return Ok(None);