use std::collections::BTreeMap; use aether_contracts::{ExecutionPlan, ExecutionTimeouts, ProxySnapshot}; use serde::{Deserialize, Serialize}; use crate::contracts::ExecutionRuntimeAuthContext; #[derive(Debug, Serialize)] pub struct GatewayControlPlanRequest { pub trace_id: String, pub method: String, pub path: String, pub query_string: Option, pub headers: BTreeMap, pub body_json: serde_json::Value, #[serde(skip_serializing_if = "Option::is_none")] pub body_base64: Option, pub auth_context: Option, } #[derive(Debug, Deserialize, Serialize)] pub struct GatewayControlPlanResponse { 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 GatewayControlSyncDecisionResponse { 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 tls_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 LocalSyncPlanAndReport { pub plan: ExecutionPlan, pub report_kind: Option, pub report_context: Option, } #[derive(Debug)] pub struct LocalStreamPlanAndReport { pub plan: ExecutionPlan, pub report_kind: Option, pub report_context: Option, } #[allow(clippy::too_many_arguments)] pub fn build_gateway_control_plan_request( trace_id: &str, method: &str, path: &str, query_string: Option<&str>, headers: BTreeMap, body_json: serde_json::Value, body_base64: Option, auth_context: Option, ) -> GatewayControlPlanRequest { GatewayControlPlanRequest { trace_id: trace_id.to_string(), method: method.to_string(), path: path.to_string(), query_string: query_string.map(ToOwned::to_owned), headers, body_json, body_base64, auth_context, } } 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)?, ); report_context.insert( "provider_request_body".to_string(), provider_request_body.clone(), ); Ok(Some(serde_json::Value::Object(report_context))) } fn decision_has_exact_provider_request(payload: &GatewayControlSyncDecisionResponse) -> 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: &GatewayControlSyncDecisionResponse, ) -> bool { !decision_has_exact_provider_request(payload) } #[cfg(test)] mod tests { use std::collections::BTreeMap; use super::{ augment_sync_report_context, build_gateway_control_plan_request, generic_decision_missing_exact_provider_request, ExecutionRuntimeAuthContext, GatewayControlSyncDecisionResponse, }; #[test] fn generic_decision_detects_missing_exact_provider_request() { let payload = GatewayControlSyncDecisionResponse { 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, tls_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_shape() { 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.get("trace_id"), Some(&serde_json::json!("abc")) ); assert_eq!( report_context.get("provider_request_body"), Some(&serde_json::json!({"model": "gpt-5"})) ); assert_eq!( report_context .get("provider_request_headers") .and_then(|value| value.get("content-type")), Some(&serde_json::json!("application/json")) ); } #[test] fn build_gateway_control_plan_request_preserves_request_shape() { let payload = build_gateway_control_plan_request( "trace-123", "POST", "/v1/chat/completions", Some("stream=true"), BTreeMap::from([("content-type".to_string(), "application/json".to_string())]), serde_json::json!({"model": "gpt-5"}), Some("eyJmb28iOiJiYXIifQ==".to_string()), Some(ExecutionRuntimeAuthContext { user_id: "user-1".to_string(), api_key_id: "key-1".to_string(), username: None, api_key_name: None, balance_remaining: Some(12.5), access_allowed: true, }), ); assert_eq!(payload.trace_id, "trace-123"); assert_eq!(payload.method, "POST"); assert_eq!(payload.path, "/v1/chat/completions"); assert_eq!(payload.query_string.as_deref(), Some("stream=true")); assert_eq!( payload.headers.get("content-type").map(String::as_str), Some("application/json") ); assert_eq!(payload.body_json, serde_json::json!({"model": "gpt-5"})); assert_eq!(payload.body_base64.as_deref(), Some("eyJmb28iOiJiYXIifQ==")); assert_eq!( payload .auth_context .as_ref() .map(|ctx| ctx.user_id.as_str()), Some("user-1") ); } }