mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-13 22:50:19 +08:00
fix(responses): replay DeepSeek opaque reasoning state
This commit is contained in:
@@ -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,
|
||||
|
||||
+7
-3
@@ -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()
|
||||
{
|
||||
|
||||
@@ -72,12 +72,15 @@ pub use crate::formats::openai::{
|
||||
request_contract::{
|
||||
finalize_openai_provider_request,
|
||||
finalize_openai_provider_request_with_codex_model_capabilities,
|
||||
finalize_openai_provider_request_with_codex_model_capabilities_and_reasoning_replay_policy,
|
||||
validate_openai_provider_request_contract, OpenAiProviderRequestContractViolation,
|
||||
OpenAiProviderRequestFinalization,
|
||||
},
|
||||
responses::{
|
||||
openai_responses_synthetic_reasoning_item_id,
|
||||
strip_incompatible_openai_responses_reasoning_items,
|
||||
strip_incompatible_openai_responses_reasoning_items_with_policy,
|
||||
OpenAiResponsesReasoningReplayPolicy,
|
||||
},
|
||||
};
|
||||
pub use crate::formats::shared::error_body::{
|
||||
|
||||
@@ -27,13 +27,32 @@ pub fn finalize_openai_provider_request(
|
||||
body: &mut Value,
|
||||
finalization: OpenAiProviderRequestFinalization<'_>,
|
||||
) -> Result<(), OpenAiProviderRequestContractViolation> {
|
||||
finalize_openai_provider_request_with_codex_model_capabilities(body, finalization, None)
|
||||
finalize_openai_provider_request_with_codex_model_capabilities_and_reasoning_replay_policy(
|
||||
body,
|
||||
finalization,
|
||||
None,
|
||||
super::responses::OpenAiResponsesReasoningReplayPolicy::OpenAiItemIds,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn finalize_openai_provider_request_with_codex_model_capabilities(
|
||||
body: &mut Value,
|
||||
finalization: OpenAiProviderRequestFinalization<'_>,
|
||||
model_capabilities: Option<&super::responses::codex::CodexResponsesModelCapabilities>,
|
||||
) -> Result<(), OpenAiProviderRequestContractViolation> {
|
||||
finalize_openai_provider_request_with_codex_model_capabilities_and_reasoning_replay_policy(
|
||||
body,
|
||||
finalization,
|
||||
model_capabilities,
|
||||
super::responses::OpenAiResponsesReasoningReplayPolicy::OpenAiItemIds,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn finalize_openai_provider_request_with_codex_model_capabilities_and_reasoning_replay_policy(
|
||||
body: &mut Value,
|
||||
finalization: OpenAiProviderRequestFinalization<'_>,
|
||||
model_capabilities: Option<&super::responses::codex::CodexResponsesModelCapabilities>,
|
||||
reasoning_replay_policy: super::responses::OpenAiResponsesReasoningReplayPolicy,
|
||||
) -> Result<(), OpenAiProviderRequestContractViolation> {
|
||||
let is_codex_reasoning_endpoint = finalization
|
||||
.provider_type
|
||||
@@ -80,9 +99,10 @@ pub fn finalize_openai_provider_request_with_codex_model_capabilities(
|
||||
body,
|
||||
finalization.provider_api_format,
|
||||
);
|
||||
super::responses::strip_incompatible_openai_responses_reasoning_items(
|
||||
super::responses::strip_incompatible_openai_responses_reasoning_items_with_policy(
|
||||
body,
|
||||
finalization.provider_api_format,
|
||||
reasoning_replay_policy,
|
||||
);
|
||||
crate::enforce_request_body_stream_field(
|
||||
body,
|
||||
|
||||
@@ -10,6 +10,18 @@ pub mod stream;
|
||||
const TOOL_ERROR_PREFIX: &str = "[tool error]";
|
||||
const AETHER_REASONING_ITEM_ID_PREFIX: &str = "rs_aether_";
|
||||
|
||||
/// Controls which provider-owned reasoning items may be replayed on a Responses request.
|
||||
///
|
||||
/// OpenAI reasoning references are identified by their `rs...` item IDs. DeepSeek's Responses
|
||||
/// contract instead returns opaque, id-less `reasoning_text` items whose `encrypted_content`
|
||||
/// must be sent back unchanged on later tool turns.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub enum OpenAiResponsesReasoningReplayPolicy {
|
||||
#[default]
|
||||
OpenAiItemIds,
|
||||
DeepSeekOpaque,
|
||||
}
|
||||
|
||||
/// Builds a stable, wire-compatible ID for a reasoning item synthesized by Aether.
|
||||
///
|
||||
/// The marker lets the outbound request sanitizer distinguish synthetic summaries from
|
||||
@@ -35,6 +47,18 @@ pub fn openai_responses_synthetic_reasoning_item_id(
|
||||
pub fn strip_incompatible_openai_responses_reasoning_items(
|
||||
body: &mut Value,
|
||||
provider_api_format: &str,
|
||||
) -> usize {
|
||||
strip_incompatible_openai_responses_reasoning_items_with_policy(
|
||||
body,
|
||||
provider_api_format,
|
||||
OpenAiResponsesReasoningReplayPolicy::OpenAiItemIds,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn strip_incompatible_openai_responses_reasoning_items_with_policy(
|
||||
body: &mut Value,
|
||||
provider_api_format: &str,
|
||||
policy: OpenAiResponsesReasoningReplayPolicy,
|
||||
) -> usize {
|
||||
if !aether_ai_formats::is_openai_responses_family_format(provider_api_format) {
|
||||
return 0;
|
||||
@@ -43,17 +67,25 @@ pub fn strip_incompatible_openai_responses_reasoning_items(
|
||||
return 0;
|
||||
};
|
||||
let original_len = items.len();
|
||||
items.retain(openai_responses_reasoning_item_is_replayable);
|
||||
items.retain(|item| openai_responses_reasoning_item_is_replayable(item, policy));
|
||||
original_len.saturating_sub(items.len())
|
||||
}
|
||||
|
||||
fn openai_responses_reasoning_item_is_replayable(item: &Value) -> bool {
|
||||
fn openai_responses_reasoning_item_is_replayable(
|
||||
item: &Value,
|
||||
policy: OpenAiResponsesReasoningReplayPolicy,
|
||||
) -> bool {
|
||||
let Some(object) = item.as_object() else {
|
||||
return true;
|
||||
};
|
||||
if object.get("type").and_then(Value::as_str) != Some("reasoning") {
|
||||
return true;
|
||||
}
|
||||
if policy == OpenAiResponsesReasoningReplayPolicy::DeepSeekOpaque
|
||||
&& deepseek_opaque_reasoning_item_is_replayable(object)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
let Some(id) = object
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
@@ -71,6 +103,33 @@ fn openai_responses_reasoning_item_is_replayable(item: &Value) -> bool {
|
||||
.is_some_and(|encrypted_content| !encrypted_content.trim().is_empty())
|
||||
}
|
||||
|
||||
fn deepseek_opaque_reasoning_item_is_replayable(object: &serde_json::Map<String, Value>) -> bool {
|
||||
if let Some(id) = object.get("id") {
|
||||
let id_is_empty = id.is_null() || id.as_str().is_some_and(|value| value.trim().is_empty());
|
||||
if !id_is_empty {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let has_encrypted_content = object
|
||||
.get("encrypted_content")
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|value| !value.trim().is_empty());
|
||||
let has_reasoning_text =
|
||||
object
|
||||
.get("content")
|
||||
.and_then(Value::as_array)
|
||||
.is_some_and(|content| {
|
||||
content.iter().any(|part| {
|
||||
part.get("type").and_then(Value::as_str) == Some("reasoning_text")
|
||||
&& part
|
||||
.get("text")
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|text| !text.is_empty())
|
||||
})
|
||||
});
|
||||
has_encrypted_content && has_reasoning_text
|
||||
}
|
||||
|
||||
/// Semantic operation carried by an OpenAI Responses request that asks the
|
||||
/// service to compact a thread. The request still uses the Responses wire
|
||||
/// contract and transport endpoint.
|
||||
@@ -121,7 +180,9 @@ mod tests {
|
||||
|
||||
use super::{
|
||||
openai_responses_request_operation, openai_responses_synthetic_reasoning_item_id,
|
||||
strip_incompatible_openai_responses_reasoning_items, OPENAI_RESPONSES_OPERATION_COMPACT,
|
||||
strip_incompatible_openai_responses_reasoning_items,
|
||||
strip_incompatible_openai_responses_reasoning_items_with_policy,
|
||||
OpenAiResponsesReasoningReplayPolicy, OPENAI_RESPONSES_OPERATION_COMPACT,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -210,4 +271,85 @@ mod tests {
|
||||
);
|
||||
assert_eq!(body["input"].as_array().map(Vec::len), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deepseek_policy_preserves_idless_opaque_reasoning_text_only_for_deepseek() {
|
||||
let item = json!({
|
||||
"type": "reasoning",
|
||||
"encrypted_content": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"content": [{
|
||||
"type": "reasoning_text",
|
||||
"text": "opaque provider reasoning that must be replayed"
|
||||
}]
|
||||
});
|
||||
let mut strict = json!({"input": [item.clone()]});
|
||||
let mut deepseek = json!({"input": [item]});
|
||||
|
||||
assert_eq!(
|
||||
strip_incompatible_openai_responses_reasoning_items_with_policy(
|
||||
&mut strict,
|
||||
"openai:responses",
|
||||
OpenAiResponsesReasoningReplayPolicy::OpenAiItemIds,
|
||||
),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
strip_incompatible_openai_responses_reasoning_items_with_policy(
|
||||
&mut deepseek,
|
||||
"openai:responses",
|
||||
OpenAiResponsesReasoningReplayPolicy::DeepSeekOpaque,
|
||||
),
|
||||
0
|
||||
);
|
||||
assert_eq!(deepseek["input"].as_array().map(Vec::len), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deepseek_policy_does_not_preserve_unbound_reasoning_summaries() {
|
||||
let mut body = json!({
|
||||
"input": [
|
||||
{
|
||||
"type": "reasoning",
|
||||
"content": [{"type": "reasoning_text", "text": "missing state"}]
|
||||
},
|
||||
{
|
||||
"type": "reasoning",
|
||||
"encrypted_content": "opaque-without-reasoning-text",
|
||||
"summary": [{"type": "summary_text", "text": "summary"}]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
strip_incompatible_openai_responses_reasoning_items_with_policy(
|
||||
&mut body,
|
||||
"openai:responses",
|
||||
OpenAiResponsesReasoningReplayPolicy::DeepSeekOpaque,
|
||||
),
|
||||
2
|
||||
);
|
||||
assert_eq!(body["input"].as_array().map(Vec::len), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deepseek_policy_does_not_preserve_opaque_item_with_foreign_id() {
|
||||
let mut body = json!({
|
||||
"input": [{
|
||||
"type": "reasoning",
|
||||
"id": "item_provider_owned",
|
||||
"encrypted_content": "opaque-state",
|
||||
"content": [{"type": "reasoning_text", "text": "thinking"}]
|
||||
}]
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
strip_incompatible_openai_responses_reasoning_items_with_policy(
|
||||
&mut body,
|
||||
"openai:responses",
|
||||
OpenAiResponsesReasoningReplayPolicy::DeepSeekOpaque,
|
||||
),
|
||||
1
|
||||
);
|
||||
assert_eq!(body["input"].as_array().map(Vec::len), Some(0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ pub fn build_standard_request_body_with_model_directives(
|
||||
user_api_key_id,
|
||||
None,
|
||||
enable_model_directives,
|
||||
crate::formats::openai::responses::OpenAiResponsesReasoningReplayPolicy::OpenAiItemIds,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -89,6 +90,7 @@ pub fn build_standard_request_body_with_model_directives_and_request_headers(
|
||||
user_api_key_id: Option<&str>,
|
||||
request_headers: Option<&http::HeaderMap>,
|
||||
enable_model_directives: bool,
|
||||
reasoning_replay_policy: crate::formats::openai::responses::OpenAiResponsesReasoningReplayPolicy,
|
||||
) -> Option<Value> {
|
||||
let mut format_context = FormatContext::default()
|
||||
.with_mapped_model(mapped_model)
|
||||
@@ -102,13 +104,26 @@ pub fn build_standard_request_body_with_model_directives_and_request_headers(
|
||||
client_api_format,
|
||||
provider_api_format,
|
||||
);
|
||||
let mut provider_request_body = convert_request(
|
||||
source_api_format.as_ref(),
|
||||
provider_api_format,
|
||||
body_json,
|
||||
&format_context,
|
||||
)
|
||||
.ok()?;
|
||||
// A same-family Responses hop is a wire-preserving route. Parsing through the
|
||||
// canonical request model here would intentionally discard provider-owned
|
||||
// input item fields (for example DeepSeek's id-less `reasoning_text` and
|
||||
// future opaque capability fields), even though no format conversion is
|
||||
// required. Keep the original object and only rewrite the routing model;
|
||||
// the normal provider-contract and compatibility passes below still apply.
|
||||
let mut provider_request_body =
|
||||
if is_same_openai_responses_family(source_api_format.as_ref(), provider_api_format) {
|
||||
let mut object = body_json.as_object()?.clone();
|
||||
object.insert("model".to_string(), Value::String(mapped_model.to_string()));
|
||||
Value::Object(object)
|
||||
} else {
|
||||
convert_request(
|
||||
source_api_format.as_ref(),
|
||||
provider_api_format,
|
||||
body_json,
|
||||
&format_context,
|
||||
)
|
||||
.ok()?
|
||||
};
|
||||
|
||||
if enable_model_directives {
|
||||
apply_model_directive_overrides_from_request(
|
||||
@@ -153,9 +168,10 @@ pub fn build_standard_request_body_with_model_directives_and_request_headers(
|
||||
&mut provider_request_body,
|
||||
provider_api_format,
|
||||
);
|
||||
crate::formats::openai::responses::strip_incompatible_openai_responses_reasoning_items(
|
||||
crate::formats::openai::responses::strip_incompatible_openai_responses_reasoning_items_with_policy(
|
||||
&mut provider_request_body,
|
||||
provider_api_format,
|
||||
reasoning_replay_policy,
|
||||
);
|
||||
strip_openai_responses_input_content_cache_control(
|
||||
&mut provider_request_body,
|
||||
@@ -176,6 +192,16 @@ pub fn build_standard_request_body_with_model_directives_and_request_headers(
|
||||
Some(provider_request_body)
|
||||
}
|
||||
|
||||
fn is_same_openai_responses_family(source_api_format: &str, provider_api_format: &str) -> bool {
|
||||
matches!(
|
||||
aether_ai_formats::normalize_api_format_alias(source_api_format).as_str(),
|
||||
"openai:responses" | "openai:responses:compact"
|
||||
) && matches!(
|
||||
aether_ai_formats::normalize_api_format_alias(provider_api_format).as_str(),
|
||||
"openai:responses" | "openai:responses:compact"
|
||||
)
|
||||
}
|
||||
|
||||
fn compatible_source_format_for_standard_request<'a>(
|
||||
body_json: &Value,
|
||||
client_api_format: &'a str,
|
||||
@@ -352,8 +378,10 @@ mod tests {
|
||||
use super::{
|
||||
build_standard_request_body, build_standard_request_body_from_canonical,
|
||||
build_standard_request_body_with_model_directives,
|
||||
build_standard_request_body_with_model_directives_and_request_headers,
|
||||
normalize_standard_request_to_openai_chat_request,
|
||||
};
|
||||
use crate::formats::openai::responses::OpenAiResponsesReasoningReplayPolicy;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
const STANDARD_SURFACES: &[&str] = &[
|
||||
@@ -500,6 +528,84 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_responses_family_matrix_preserves_opaque_reasoning_items_and_unknown_fields() {
|
||||
let input = (0..66)
|
||||
.map(|index| {
|
||||
json!({
|
||||
"type": "reasoning",
|
||||
"encrypted_content": format!("opaque-state-{index}"),
|
||||
"content": [{
|
||||
"type": "reasoning_text",
|
||||
"text": format!("thinking {index}")
|
||||
}],
|
||||
"future_capability": {"budget_class": "dynamic"}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let request = json!({
|
||||
"model": "deepseek-v4-flash",
|
||||
"input": input,
|
||||
"future_request_field": {"preserve": true}
|
||||
});
|
||||
|
||||
let preserved = build_standard_request_body_with_model_directives_and_request_headers(
|
||||
&request,
|
||||
"openai:responses",
|
||||
"deepseek-v4-flash",
|
||||
"custom",
|
||||
"openai:responses",
|
||||
"/v1/responses",
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
OpenAiResponsesReasoningReplayPolicy::DeepSeekOpaque,
|
||||
)
|
||||
.expect("same-family Responses body should build");
|
||||
assert_eq!(preserved["model"], "deepseek-v4-flash");
|
||||
assert_eq!(preserved["future_request_field"]["preserve"], true);
|
||||
assert_eq!(
|
||||
preserved["input"]
|
||||
.as_array()
|
||||
.expect("input array")
|
||||
.iter()
|
||||
.filter(|item| item["type"] == "reasoning")
|
||||
.count(),
|
||||
66
|
||||
);
|
||||
assert_eq!(
|
||||
preserved["input"][0]["future_capability"]["budget_class"],
|
||||
"dynamic"
|
||||
);
|
||||
|
||||
let strict = build_standard_request_body_with_model_directives_and_request_headers(
|
||||
&request,
|
||||
"openai:responses",
|
||||
"deepseek-v4-flash",
|
||||
"openai",
|
||||
"openai:responses",
|
||||
"/v1/responses",
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
OpenAiResponsesReasoningReplayPolicy::OpenAiItemIds,
|
||||
)
|
||||
.expect("strict same-family Responses body should build");
|
||||
assert_eq!(
|
||||
strict["input"]
|
||||
.as_array()
|
||||
.expect("strict input array")
|
||||
.iter()
|
||||
.filter(|item| item["type"] == "reasoning")
|
||||
.count(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_request_body_scopes_previous_response_history_by_api_key() {
|
||||
record_converted_response_history(
|
||||
|
||||
@@ -303,10 +303,6 @@ pub fn build_local_openai_responses_request_body_with_model_directives(
|
||||
require_streaming,
|
||||
require_body_stream_field,
|
||||
);
|
||||
crate::formats::openai::responses::strip_incompatible_openai_responses_reasoning_items(
|
||||
&mut provider_request_body,
|
||||
"openai:responses",
|
||||
);
|
||||
Some(provider_request_body)
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ pub use formats::openai::reasoning::{
|
||||
pub use formats::openai::request_contract::{
|
||||
finalize_openai_provider_request,
|
||||
finalize_openai_provider_request_with_codex_model_capabilities,
|
||||
finalize_openai_provider_request_with_codex_model_capabilities_and_reasoning_replay_policy,
|
||||
validate_openai_provider_request_contract, OpenAiProviderRequestContractViolation,
|
||||
OpenAiProviderRequestFinalization,
|
||||
};
|
||||
@@ -54,7 +55,9 @@ pub use formats::openai::responses::request::{
|
||||
};
|
||||
pub use formats::openai::responses::{
|
||||
openai_responses_request_operation, openai_responses_synthetic_reasoning_item_id,
|
||||
strip_incompatible_openai_responses_reasoning_items, OPENAI_RESPONSES_OPERATION_COMPACT,
|
||||
strip_incompatible_openai_responses_reasoning_items,
|
||||
strip_incompatible_openai_responses_reasoning_items_with_policy,
|
||||
OpenAiResponsesReasoningReplayPolicy, OPENAI_RESPONSES_OPERATION_COMPACT,
|
||||
};
|
||||
pub use formats::registry::{
|
||||
build_stream_transcoder, convert_request, convert_request_pure,
|
||||
|
||||
@@ -145,6 +145,7 @@ pub use rules::{
|
||||
pub use same_format_provider::{
|
||||
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, classify_same_format_provider_request_behavior,
|
||||
classify_same_format_provider_request_behavior_for_operation,
|
||||
enforce_same_format_provider_api_operation_body_policy,
|
||||
|
||||
@@ -207,15 +207,32 @@ pub fn classify_same_format_provider_request_behavior_for_operation(
|
||||
pub fn build_same_format_provider_request_body(
|
||||
input: SameFormatProviderRequestBodyInput<'_>,
|
||||
) -> Option<Value> {
|
||||
build_same_format_provider_request_body_inner(input, None)
|
||||
build_same_format_provider_request_body_inner(
|
||||
input,
|
||||
None,
|
||||
aether_ai_formats::OpenAiResponsesReasoningReplayPolicy::OpenAiItemIds,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn build_same_format_provider_request_body_with_compatibility_report(
|
||||
input: SameFormatProviderRequestBodyInput<'_>,
|
||||
) -> Option<SameFormatProviderRequestBodyOutput> {
|
||||
build_same_format_provider_request_body_with_compatibility_report_and_reasoning_replay_policy(
|
||||
input,
|
||||
aether_ai_formats::OpenAiResponsesReasoningReplayPolicy::OpenAiItemIds,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn build_same_format_provider_request_body_with_compatibility_report_and_reasoning_replay_policy(
|
||||
input: SameFormatProviderRequestBodyInput<'_>,
|
||||
reasoning_replay_policy: aether_ai_formats::OpenAiResponsesReasoningReplayPolicy,
|
||||
) -> Option<SameFormatProviderRequestBodyOutput> {
|
||||
let mut compatibility_edits = Vec::new();
|
||||
let body =
|
||||
build_same_format_provider_request_body_inner(input, Some(&mut compatibility_edits))?;
|
||||
let body = build_same_format_provider_request_body_inner(
|
||||
input,
|
||||
Some(&mut compatibility_edits),
|
||||
reasoning_replay_policy,
|
||||
)?;
|
||||
Some(SameFormatProviderRequestBodyOutput {
|
||||
body,
|
||||
compatibility_edits,
|
||||
@@ -239,6 +256,7 @@ pub fn enforce_same_format_provider_api_operation_body_policy(
|
||||
fn build_same_format_provider_request_body_inner(
|
||||
input: SameFormatProviderRequestBodyInput<'_>,
|
||||
mut compatibility_edits: Option<&mut Vec<SameFormatProviderCompatibilityEdit>>,
|
||||
reasoning_replay_policy: aether_ai_formats::OpenAiResponsesReasoningReplayPolicy,
|
||||
) -> Option<Value> {
|
||||
if let Some(kiro_auth_config) = input.kiro_auth_config {
|
||||
let body = build_kiro_provider_request_body(
|
||||
@@ -437,9 +455,10 @@ fn build_same_format_provider_request_body_inner(
|
||||
);
|
||||
}
|
||||
let stripped_reasoning_items =
|
||||
aether_ai_formats::strip_incompatible_openai_responses_reasoning_items(
|
||||
aether_ai_formats::strip_incompatible_openai_responses_reasoning_items_with_policy(
|
||||
&mut provider_request_body,
|
||||
input.provider_api_format,
|
||||
reasoning_replay_policy,
|
||||
);
|
||||
if stripped_reasoning_items > 0 {
|
||||
record_compatibility_edit(
|
||||
@@ -466,7 +485,7 @@ fn build_same_format_provider_request_body_inner(
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
if aether_ai_formats::finalize_openai_provider_request(
|
||||
if aether_ai_formats::finalize_openai_provider_request_with_codex_model_capabilities_and_reasoning_replay_policy(
|
||||
&mut provider_request_body,
|
||||
aether_ai_formats::OpenAiProviderRequestFinalization {
|
||||
source_api_format: input.provider_api_format,
|
||||
@@ -478,6 +497,8 @@ fn build_same_format_provider_request_body_inner(
|
||||
upstream_is_stream: input.upstream_is_stream,
|
||||
require_body_stream_field,
|
||||
},
|
||||
None,
|
||||
reasoning_replay_policy,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
@@ -2099,6 +2120,74 @@ mod tests {
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_format_responses_body_preserves_deepseek_opaque_reasoning_replay() {
|
||||
let reasoning_items = (0..66)
|
||||
.map(|index| {
|
||||
json!({
|
||||
"type": "reasoning",
|
||||
"encrypted_content": format!("opaque-deepseek-state-{index}"),
|
||||
"content": [{
|
||||
"type": "reasoning_text",
|
||||
"text": format!("provider thinking state {index}")
|
||||
}]
|
||||
})
|
||||
})
|
||||
.chain(std::iter::once(json!({
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": "continue"
|
||||
})))
|
||||
.collect::<Vec<_>>();
|
||||
let request_body = json!({
|
||||
"model": "deepseek-v4-flash",
|
||||
"input": reasoning_items
|
||||
});
|
||||
let input = SameFormatProviderRequestBodyInput {
|
||||
body_json: &request_body,
|
||||
mapped_model: "deepseek-v4-flash",
|
||||
client_api_format: "openai:responses",
|
||||
provider_api_format: "openai:responses",
|
||||
source_model: Some("deepseek-v4-flash"),
|
||||
family: SameFormatProviderFamily::Standard,
|
||||
body_rules: None,
|
||||
request_headers: None,
|
||||
upstream_is_stream: false,
|
||||
force_body_stream_field: false,
|
||||
kiro_auth_config: None,
|
||||
is_claude_code: false,
|
||||
enable_model_directives: false,
|
||||
};
|
||||
|
||||
let deepseek =
|
||||
build_same_format_provider_request_body_with_compatibility_report_and_reasoning_replay_policy(
|
||||
input,
|
||||
aether_ai_formats::OpenAiResponsesReasoningReplayPolicy::DeepSeekOpaque,
|
||||
)
|
||||
.expect("DeepSeek same-format Responses body should build");
|
||||
let strict = build_same_format_provider_request_body_with_compatibility_report(input)
|
||||
.expect("strict same-format Responses body should build");
|
||||
|
||||
assert_eq!(
|
||||
deepseek.body["input"]
|
||||
.as_array()
|
||||
.expect("DeepSeek input array")
|
||||
.iter()
|
||||
.filter(|item| item["type"] == "reasoning")
|
||||
.count(),
|
||||
66
|
||||
);
|
||||
assert_eq!(
|
||||
strict.body["input"]
|
||||
.as_array()
|
||||
.expect("strict input array")
|
||||
.iter()
|
||||
.filter(|item| item["type"] == "reasoning")
|
||||
.count(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_format_stream_policy_wins_after_body_rules() {
|
||||
let body_rules = json!([
|
||||
|
||||
Reference in New Issue
Block a user