mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 18:07:47 +08:00
fix(ai): harden responses replay and failure diagnostics
This commit is contained in:
@@ -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!({
|
||||
|
||||
+18
-25
@@ -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);
|
||||
|
||||
+24
-26
@@ -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);
|
||||
|
||||
@@ -171,7 +171,9 @@ pub(crate) use aether_ai_formats::api::{
|
||||
pub(crate) use aether_ai_formats::{
|
||||
api_format_defaults_to_client_error_failover, api_format_defaults_to_non_stream,
|
||||
api_format_permission_covers, intersect_api_format_allowed_lists, is_embedding_api_format,
|
||||
is_rerank_api_format, openai_responses_request_operation, ApiOperation, ClientSurface,
|
||||
is_rerank_api_format, openai_responses_request_operation,
|
||||
openai_responses_synthetic_reasoning_item_id,
|
||||
strip_incompatible_openai_responses_reasoning_items, ApiOperation, ClientSurface,
|
||||
};
|
||||
|
||||
pub(crate) fn plan_kind_matches_api_operation(
|
||||
|
||||
Reference in New Issue
Block a user