fix(responses): replay DeepSeek opaque reasoning state

This commit is contained in:
ZheFox
2026-08-20 00:40:48 +08:00
parent d21d8ce9f5
commit bef282cfee
22 changed files with 516 additions and 51 deletions
@@ -23,6 +23,7 @@ pub(crate) use self::finalize::internal::{
maybe_build_sync_finalize_outcome, maybe_compile_sync_finalize_response,
SyncToStreamBridgeOutcome,
};
pub(crate) use self::planner::openai_responses_reasoning_replay_policy;
pub(crate) use self::planner::{
apply_local_runtime_candidate_terminal_reason, build_gemini_stream_plan_from_decision,
build_gemini_sync_plan_from_decision, build_local_gemini_files_stream_attempt_source_for_kind,
@@ -257,7 +257,7 @@ pub(crate) fn apply_provider_request_routing_policy_to_decision(
input.requested_model.as_str(),
)
});
crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities(
crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities_and_reasoning_replay_policy(
&mut provider_request_body,
crate::ai_serving::OpenAiProviderRequestFinalization {
source_api_format: context.client_api_format.as_str(),
@@ -272,6 +272,14 @@ pub(crate) fn apply_provider_request_routing_policy_to_decision(
.is_some_and(|body| body.get("stream").is_some()),
},
model_capabilities.as_ref(),
transport
.map(|transport| {
crate::ai_serving::openai_responses_reasoning_replay_policy(
transport.provider.provider_type.as_str(),
transport.endpoint.base_url.as_str(),
)
})
.unwrap_or_default(),
)
.map_err(|violation| GatewayError::Client {
status: StatusCode::BAD_REQUEST,
@@ -82,9 +82,9 @@ pub(crate) use self::standard::{
build_local_sync_attempt_source as build_standard_family_sync_attempt_source,
build_local_sync_plan_and_reports as build_standard_family_sync_plan_and_reports,
codex_model_capabilities_for_transport, maybe_build_responses_websocket_decision,
set_local_openai_chat_execution_exhausted_diagnostic, validate_final_openai_provider_request,
ResponsesWebSocketBodyNormalization, ResponsesWebSocketDecision,
ResponsesWebSocketPinnedCandidate,
openai_responses_reasoning_replay_policy, set_local_openai_chat_execution_exhausted_diagnostic,
validate_final_openai_provider_request, ResponsesWebSocketBodyNormalization,
ResponsesWebSocketDecision, ResponsesWebSocketPinnedCandidate,
};
pub(crate) use self::state::{
GatewayAuthApiKeySnapshot, GatewayProviderTransportSnapshot, LocalResolvedOAuthRequestAuth,
@@ -42,7 +42,7 @@ use super::{
};
use crate::ai_serving::planner::standard::{
codex_model_capabilities_for_transport, openai_provider_request_contract_failure_extra_data,
same_format_provider_request_body_failure_extra_data,
openai_responses_reasoning_replay_policy, same_format_provider_request_body_failure_extra_data,
};
pub(crate) fn resolve_same_format_provider_transport_unsupported_reason_for_trace(
@@ -205,6 +205,10 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts(
prepared.kiro_auth.as_ref(),
prepared.is_claude_code,
false,
openai_responses_reasoning_replay_policy(
prepared.transport.provider.provider_type.as_str(),
prepared.transport.endpoint.base_url.as_str(),
),
)
else {
mark_skipped_local_same_format_provider_candidate_with_extra_data(
@@ -269,7 +273,7 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts(
source_model,
);
if let Err(violation) =
crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities(
crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities_and_reasoning_replay_policy(
&mut base_provider_request_body,
crate::ai_serving::OpenAiProviderRequestFinalization {
source_api_format: spec.api_format,
@@ -285,6 +289,10 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts(
),
},
codex_model_capabilities.as_ref(),
openai_responses_reasoning_replay_policy(
transport.provider.provider_type.as_str(),
transport.endpoint.base_url.as_str(),
),
)
{
mark_skipped_local_same_format_provider_candidate_with_extra_data(
@@ -3,7 +3,7 @@ use serde_json::Value;
use super::super::LocalSameFormatProviderSpec;
use crate::ai_serving::transport::{
build_same_format_provider_request_body as build_same_format_provider_request_body_impl,
build_same_format_provider_request_body_with_compatibility_report as build_same_format_provider_request_body_with_compatibility_report_impl,
build_same_format_provider_request_body_with_compatibility_report_and_reasoning_replay_policy as build_same_format_provider_request_body_with_compatibility_report_impl,
SameFormatProviderFamily, SameFormatProviderRequestBodyInput,
SameFormatProviderRequestBodyOutput,
};
@@ -50,6 +50,7 @@ pub(crate) fn build_same_format_provider_request_body_with_compatibility_report(
kiro_auth: Option<&crate::ai_serving::transport::kiro::KiroRequestAuth>,
is_claude_code: bool,
enable_model_directives: bool,
reasoning_replay_policy: crate::ai_serving::OpenAiResponsesReasoningReplayPolicy,
) -> Option<SameFormatProviderRequestBodyOutput> {
build_same_format_provider_request_body_with_compatibility_report_impl(
SameFormatProviderRequestBodyInput {
@@ -67,6 +68,7 @@ pub(crate) fn build_same_format_provider_request_body_with_compatibility_report(
is_claude_code,
enable_model_directives,
},
reasoning_replay_policy,
)
}
@@ -13,6 +13,17 @@ pub(crate) fn is_deepseek_provider(provider_type: &str, base_url: &str) -> bool
host == "deepseek.com" || host.ends_with(".deepseek.com")
}
pub(crate) fn openai_responses_reasoning_replay_policy(
provider_type: &str,
base_url: &str,
) -> crate::ai_serving::OpenAiResponsesReasoningReplayPolicy {
if is_deepseek_provider(provider_type, base_url) {
crate::ai_serving::OpenAiResponsesReasoningReplayPolicy::DeepSeekOpaque
} else {
crate::ai_serving::OpenAiResponsesReasoningReplayPolicy::OpenAiItemIds
}
}
pub(crate) fn apply_deepseek_tool_call_thinking_compat(
provider_request_body: &mut Value,
provider_type: &str,
@@ -241,7 +252,10 @@ fn is_claude_thinking_block(block: &Value) -> bool {
mod tests {
use serde_json::json;
use super::{apply_deepseek_tool_call_thinking_compat, is_deepseek_provider};
use super::{
apply_deepseek_tool_call_thinking_compat, is_deepseek_provider,
openai_responses_reasoning_replay_policy,
};
#[test]
fn detects_deepseek_provider_by_type_or_host() {
@@ -257,6 +271,52 @@ mod tests {
"custom",
"https://example.com/deepseek"
));
assert_eq!(
openai_responses_reasoning_replay_policy("custom", "https://api.deepseek.com/v1"),
crate::ai_serving::OpenAiResponsesReasoningReplayPolicy::DeepSeekOpaque
);
assert_eq!(
openai_responses_reasoning_replay_policy("openai", "https://api.openai.com/v1"),
crate::ai_serving::OpenAiResponsesReasoningReplayPolicy::OpenAiItemIds
);
}
#[test]
fn custom_deepseek_host_preserves_production_shaped_opaque_reasoning_replay() {
let reasoning_items = (0..66)
.map(|index| {
json!({
"type": "reasoning",
"encrypted_content": format!("550e8400-e29b-41d4-a716-{index:012}"),
"content": [{
"type": "reasoning_text",
"text": format!("opaque DeepSeek reasoning {index}")
}]
})
})
.collect::<Vec<_>>();
let mut deepseek = json!({"input": reasoning_items.clone()});
let mut openai = json!({"input": reasoning_items});
assert_eq!(
aether_ai_formats::strip_incompatible_openai_responses_reasoning_items_with_policy(
&mut deepseek,
"openai:responses",
openai_responses_reasoning_replay_policy("custom", "https://api.deepseek.com/v1"),
),
0
);
assert_eq!(deepseek["input"].as_array().map(Vec::len), Some(66));
assert_eq!(
aether_ai_formats::strip_incompatible_openai_responses_reasoning_items_with_policy(
&mut openai,
"openai:responses",
openai_responses_reasoning_replay_policy("openai", "https://api.openai.com/v1"),
),
66
);
assert_eq!(openai["input"].as_array().map(Vec::len), Some(0));
}
#[test]
@@ -23,8 +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,
openai_provider_request_contract_failure_extra_data, request_body_build_failure_extra_data,
request_conversion_failure_extra_data,
openai_provider_request_contract_failure_extra_data, openai_responses_reasoning_replay_policy,
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,
@@ -620,6 +620,10 @@ pub(crate) async fn resolve_local_standard_candidate_payload_parts(
Some(input.auth_context.api_key_id.as_str()),
Some(effective_headers),
false,
openai_responses_reasoning_replay_policy(
transport.provider.provider_type.as_str(),
transport.endpoint.base_url.as_str(),
),
) {
Some(body) => body,
None => {
@@ -750,7 +754,7 @@ pub(crate) async fn resolve_local_standard_candidate_payload_parts(
source_model,
);
if let Err(violation) =
crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities(
crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities_and_reasoning_replay_policy(
&mut provider_request_body,
crate::ai_serving::OpenAiProviderRequestFinalization {
source_api_format: spec_metadata.api_format,
@@ -766,6 +770,10 @@ pub(crate) async fn resolve_local_standard_candidate_payload_parts(
),
},
codex_model_capabilities.as_ref(),
openai_responses_reasoning_replay_policy(
transport.provider.provider_type.as_str(),
transport.endpoint.base_url.as_str(),
),
)
{
mark_skipped_local_standard_candidate_with_extra_data(
@@ -18,7 +18,10 @@ pub(crate) use self::codex::{
apply_codex_openai_responses_special_body_edits, apply_codex_openai_special_headers,
codex_model_capabilities_for_transport,
};
pub(crate) use self::deepseek::{apply_deepseek_tool_call_thinking_compat, is_deepseek_provider};
pub(crate) use self::deepseek::{
apply_deepseek_tool_call_thinking_compat, is_deepseek_provider,
openai_responses_reasoning_replay_policy,
};
pub(crate) use self::family::{
build_local_stream_attempt_source, build_local_stream_plan_and_reports,
build_local_sync_attempt_source, build_local_sync_plan_and_reports,
@@ -81,10 +81,6 @@ 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,
@@ -176,10 +172,6 @@ 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,
@@ -32,8 +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,
openai_provider_request_contract_failure_extra_data, request_body_build_failure_extra_data,
request_conversion_failure_extra_data,
openai_provider_request_contract_failure_extra_data, openai_responses_reasoning_replay_policy,
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::{
@@ -532,7 +532,7 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
Some(body_json),
);
if let Err(violation) =
crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities(
crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities_and_reasoning_replay_policy(
&mut base_provider_request_body,
crate::ai_serving::OpenAiProviderRequestFinalization {
source_api_format: spec_metadata.api_format,
@@ -548,6 +548,10 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
),
},
codex_model_capabilities.as_ref(),
openai_responses_reasoning_replay_policy(
transport.provider.provider_type.as_str(),
transport.endpoint.base_url.as_str(),
),
)
{
mark_skipped_local_openai_responses_candidate_with_extra_data(
@@ -3,6 +3,7 @@ use crate::ai_serving::planner::plan_builders::{AiStreamAttempt, AiSyncAttempt};
use crate::ai_serving::planner::spec_metadata::local_openai_responses_spec_metadata;
use crate::ai_serving::planner::standard::codex::codex_model_capabilities_for_transport;
use crate::ai_serving::planner::standard::normalize::build_local_openai_responses_request_body_with_codex_model_capabilities;
use crate::ai_serving::planner::standard::openai_responses_reasoning_replay_policy;
use crate::ai_serving::GatewayControlDecision;
use crate::orchestration::{
codex_quota_breaker_blocks_candidate, log_codex_quota_breaker_check_failure,
@@ -293,6 +294,7 @@ pub(crate) struct ResponsesWebSocketBodyNormalization {
body_rules: Option<serde_json::Value>,
request_headers: http::HeaderMap,
codex_model_capabilities: Option<crate::ai_serving::CodexResponsesModelCapabilities>,
reasoning_replay_policy: crate::ai_serving::OpenAiResponsesReasoningReplayPolicy,
model_directive_patch: Option<serde_json::Value>,
}
@@ -313,6 +315,8 @@ impl ResponsesWebSocketBodyNormalization {
body_rules: None,
request_headers: http::HeaderMap::new(),
codex_model_capabilities: None,
reasoning_replay_policy:
crate::ai_serving::OpenAiResponsesReasoningReplayPolicy::OpenAiItemIds,
model_directive_patch: None,
}
}
@@ -377,7 +381,7 @@ impl ResponsesWebSocketBodyNormalization {
require_body_stream_field,
);
}
crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities(
crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities_and_reasoning_replay_policy(
&mut body,
crate::ai_serving::OpenAiProviderRequestFinalization {
source_api_format: self.client_api_format.as_str(),
@@ -390,6 +394,7 @@ impl ResponsesWebSocketBodyNormalization {
require_body_stream_field,
},
self.codex_model_capabilities.as_ref(),
self.reasoning_replay_policy,
)
.ok()?;
Some(body)
@@ -539,6 +544,10 @@ pub(crate) async fn maybe_build_responses_websocket_decision(
mapped_model.as_str(),
source_model,
),
reasoning_replay_policy: openai_responses_reasoning_replay_policy(
transport.provider.provider_type.as_str(),
transport.endpoint.base_url.as_str(),
),
model_directive_patch: input
.model_directive_policy
.resolve_reasoning(
@@ -54,6 +54,7 @@ pub(crate) use aether_ai_formats::api::{
enforce_request_body_stream_field, estimate_kiro_tokens, extract_openai_text_content,
finalize_openai_provider_request,
finalize_openai_provider_request_with_codex_model_capabilities,
finalize_openai_provider_request_with_codex_model_capabilities_and_reasoning_replay_policy,
find_kiro_real_thinking_end_tag, find_kiro_real_thinking_end_tag_at_buffer_end,
find_kiro_real_thinking_start_tag, forbid_upstream_streaming_for_provider,
force_upstream_streaming_for_provider, gemini_request_is_image_generation,
@@ -123,9 +124,9 @@ pub(crate) use aether_ai_formats::api::{
OpenAIResponsesProviderState, OpenAiImageNormalizeOptions, OpenAiImageOperation,
OpenAiImageRequestForGemini, OpenAiImageResponseFormat, OpenAiImageStreamState,
OpenAiImageSyncFinalizeProduct, OpenAiProviderRequestFinalization,
ProviderAdaptationDescriptor, ProviderAdaptationSurface, ProviderPrivateStreamNormalizer,
ReasoningEffort, RequestConversionKind, ResponseHistoryRecord, ServiceTier,
StandardCrossFormatSyncProduct, StandardSyncFinalizeNormalizedProduct,
OpenAiResponsesReasoningReplayPolicy, ProviderAdaptationDescriptor, ProviderAdaptationSurface,
ProviderPrivateStreamNormalizer, ReasoningEffort, RequestConversionKind, ResponseHistoryRecord,
ServiceTier, StandardCrossFormatSyncProduct, StandardSyncFinalizeNormalizedProduct,
StreamingStandardFormatMatrix, SyncChatResponseConversionKind, SyncCliResponseConversionKind,
SyncToStreamBridgeOutcome, ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME, CLAUDE_CHAT_STREAM_PLAN_KIND,
CLAUDE_CHAT_STREAM_SUCCESS_REPORT_KIND, CLAUDE_CHAT_SYNC_ERROR_REPORT_KIND,
@@ -75,6 +75,7 @@ pub(crate) use aether_provider_transport::{
build_request_trace_proxy_value, build_same_format_provider_headers,
build_same_format_provider_request_body,
build_same_format_provider_request_body_with_compatibility_report,
build_same_format_provider_request_body_with_compatibility_report_and_reasoning_replay_policy,
build_same_format_provider_upstream_url, build_standard_plan_fallback_headers,
build_standard_plan_fallback_openai_chat_url,
build_standard_plan_fallback_openai_responses_url, build_standard_provider_request_headers,
@@ -3174,6 +3174,10 @@ async fn provider_query_execute_standard_test_candidate(
Some(candidate.key.id.as_str()),
Some(&incoming_request_headers),
false,
crate::ai_serving::openai_responses_reasoning_replay_policy(
transport.provider.provider_type.as_str(),
transport.endpoint.base_url.as_str(),
),
)
else {
return Ok(provider_query_skipped_execution_outcome(
@@ -3218,7 +3222,7 @@ async fn provider_query_execute_standard_test_candidate(
if matches!(
normalized_provider_api_format.as_str(),
"openai:chat" | "openai:responses" | "openai:responses:compact" | "openai:search"
) && crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities(
) && crate::ai_serving::finalize_openai_provider_request_with_codex_model_capabilities_and_reasoning_replay_policy(
&mut provider_request_body,
crate::ai_serving::OpenAiProviderRequestFinalization {
source_api_format: client_api_format,
@@ -3231,6 +3235,10 @@ async fn provider_query_execute_standard_test_candidate(
require_body_stream_field,
},
codex_model_capabilities.as_ref(),
crate::ai_serving::openai_responses_reasoning_replay_policy(
transport.provider.provider_type.as_str(),
transport.endpoint.base_url.as_str(),
),
)
.is_err()
{