use std::collections::BTreeMap; use aether_ai_formats::api::ExecutionRuntimeAuthContext; use aether_contracts::{ExecutionPlan, ExecutionTimeouts, ProxySnapshot, ResolvedTransportProfile}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ExecutionStrategy { GatewayAffinityForward, RawPublicProxy, LocalSameFormat, LocalCrossFormat, } impl ExecutionStrategy { pub const fn as_str(self) -> &'static str { match self { Self::GatewayAffinityForward => "gateway_affinity_forward", Self::RawPublicProxy => "raw_public_proxy", Self::LocalSameFormat => "local_same_format", Self::LocalCrossFormat => "local_cross_format", } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ConversionMode { None, RequestOnly, ResponseOnly, Bidirectional, } impl ConversionMode { pub const fn as_str(self) -> &'static str { match self { Self::None => "none", Self::RequestOnly => "request_only", Self::ResponseOnly => "response_only", Self::Bidirectional => "bidirectional", } } } #[derive(Debug, Deserialize, Serialize)] pub struct AiExecutionPlanPayload { pub action: String, #[serde(default)] pub plan_kind: Option, #[serde(default)] pub plan: Option, #[serde(default)] pub report_kind: Option, #[serde(default)] pub report_context: Option, #[serde(default)] pub auth_context: Option, } #[derive(Debug, Deserialize, Serialize)] pub struct AiExecutionDecision { pub action: String, #[serde(default)] pub decision_kind: Option, #[serde(default)] pub execution_strategy: Option, #[serde(default)] pub conversion_mode: Option, #[serde(default)] pub request_id: Option, #[serde(default)] pub candidate_id: Option, #[serde(default)] pub provider_name: Option, #[serde(default)] pub provider_id: Option, #[serde(default)] pub endpoint_id: Option, #[serde(default)] pub key_id: Option, #[serde(default)] pub upstream_base_url: Option, #[serde(default)] pub upstream_url: Option, #[serde(default)] pub provider_request_method: Option, #[serde(default)] pub auth_header: Option, #[serde(default)] pub auth_value: Option, #[serde(default)] pub provider_api_format: Option, #[serde(default)] pub client_api_format: Option, #[serde(default)] pub provider_contract: Option, #[serde(default)] pub client_contract: Option, #[serde(default)] pub model_name: Option, #[serde(default)] pub mapped_model: Option, #[serde(default)] pub prompt_cache_key: Option, #[serde(default)] pub extra_headers: BTreeMap, #[serde(default)] pub provider_request_headers: BTreeMap, #[serde(default)] pub provider_request_body: Option, #[serde(default)] pub provider_request_body_base64: Option, #[serde(default)] pub content_type: Option, #[serde(default)] pub proxy: Option, #[serde(default)] pub transport_profile: Option, #[serde(default)] pub timeouts: Option, #[serde(default)] pub upstream_is_stream: bool, #[serde(default)] pub report_kind: Option, #[serde(default)] pub report_context: Option, #[serde(default)] pub auth_context: Option, } #[derive(Debug)] pub struct AiSyncAttempt { pub plan: ExecutionPlan, pub report_kind: Option, pub report_context: Option, } #[derive(Debug)] pub struct AiStreamAttempt { pub plan: ExecutionPlan, pub report_kind: Option, pub report_context: Option, } pub fn augment_sync_report_context( report_context: Option, provider_request_headers: &BTreeMap, _provider_request_body: &serde_json::Value, ) -> serde_json::Result> { let mut report_context = match report_context { Some(serde_json::Value::Object(map)) => map, Some(_) => serde_json::Map::new(), None => serde_json::Map::new(), }; report_context.insert( "provider_request_headers".to_string(), serde_json::to_value(provider_request_headers)?, ); Ok(Some(serde_json::Value::Object(report_context))) } fn decision_has_exact_provider_request(payload: &AiExecutionDecision) -> bool { !payload.provider_request_headers.is_empty() && (payload.provider_request_body.is_some() || payload .provider_request_body_base64 .as_ref() .map(|value| !value.trim().is_empty()) .unwrap_or(false)) } pub fn generic_decision_missing_exact_provider_request(payload: &AiExecutionDecision) -> bool { !decision_has_exact_provider_request(payload) } #[cfg(test)] mod tests { use std::collections::BTreeMap; use super::{ augment_sync_report_context, generic_decision_missing_exact_provider_request, AiExecutionDecision, }; #[test] fn generic_decision_detects_missing_exact_provider_request() { let payload = AiExecutionDecision { action: "local".to_string(), decision_kind: Some("sync".to_string()), execution_strategy: None, conversion_mode: None, request_id: None, candidate_id: None, provider_name: None, provider_id: None, endpoint_id: None, key_id: None, upstream_base_url: None, upstream_url: None, provider_request_method: None, auth_header: None, auth_value: None, provider_api_format: None, client_api_format: None, provider_contract: None, client_contract: None, model_name: None, mapped_model: None, prompt_cache_key: None, extra_headers: Default::default(), provider_request_headers: Default::default(), provider_request_body: None, provider_request_body_base64: None, content_type: None, proxy: None, transport_profile: None, timeouts: None, upstream_is_stream: false, report_kind: None, report_context: None, auth_context: None, }; assert!(generic_decision_missing_exact_provider_request(&payload)); } #[test] fn augment_sync_report_context_attaches_provider_request_headers_only() { let report_context = augment_sync_report_context( Some(serde_json::json!({"trace_id": "abc"})), &BTreeMap::from([("content-type".to_string(), "application/json".to_string())]), &serde_json::json!({"model": "gpt-5"}), ) .expect("context should serialize") .expect("context should exist"); assert_eq!( report_context["provider_request_headers"]["content-type"], "application/json" ); assert!( report_context.get("provider_request_body").is_none(), "provider request body should not be copied into report context" ); } }