use std::collections::BTreeMap; use std::fmt; use aether_ai_formats::api::ExecutionRuntimeAuthContext; use aether_contracts::{redact_url_for_debug, ExecutionPlan, RequestBody}; use url::Url; use crate::dto::{AiExecutionDecision, AiRequestGzipPolicy}; const DEFAULT_REQUEST_GZIP_MIN_JSON_BYTES: usize = 64 * 1024; #[derive(Debug, Clone, PartialEq, Eq)] pub struct AiDecisionPlanCore { pub request_id: String, pub provider_id: String, pub endpoint_id: String, pub key_id: String, pub provider_api_format: String, pub client_api_format: String, } #[derive(Clone, PartialEq, Eq)] pub struct AiUpstreamAuthPair { pub header: String, pub value: String, } impl fmt::Debug for AiUpstreamAuthPair { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("AiUpstreamAuthPair") .field("header", &self.header) .field("has_value", &(!self.value.is_empty())) .field("value_len", &self.value.len()) .finish() } } pub struct AiExecutionPlanFromDecisionParts { pub core: AiDecisionPlanCore, pub method: String, pub url: String, pub headers: BTreeMap, pub content_type: Option, pub body: RequestBody, pub stream: bool, } impl fmt::Debug for AiExecutionPlanFromDecisionParts { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("AiExecutionPlanFromDecisionParts") .field("core", &self.core) .field("method", &self.method) .field("url", &redact_url_for_debug(&self.url)) .field("header_names", &self.headers.keys().collect::>()) .field("content_type", &self.content_type) .field("body", &self.body) .field("stream", &self.stream) .finish() } } pub struct AiExecutionDecisionFromPlanParts { pub action: String, pub decision_kind: Option, pub request_id: Option, pub upstream_base_url: Option, pub include_auth_pair: bool, pub plan: ExecutionPlan, pub report_kind: Option, pub report_context: Option, pub auth_context: Option, } impl fmt::Debug for AiExecutionDecisionFromPlanParts { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("AiExecutionDecisionFromPlanParts") .field("action", &self.action) .field("decision_kind", &self.decision_kind) .field("request_id", &self.request_id) .field( "upstream_base_url", &self.upstream_base_url.as_deref().map(redact_url_for_debug), ) .field("include_auth_pair", &self.include_auth_pair) .field("plan", &self.plan) .field("report_kind", &self.report_kind) .field("has_report_context", &self.report_context.is_some()) .field( "report_context_bytes", &self .report_context .as_ref() .and_then(|value| serde_json::to_vec(value).ok().map(|bytes| bytes.len())), ) .field("has_auth_context", &self.auth_context.is_some()) .finish() } } pub fn take_ai_non_empty_string(value: &mut Option) -> Option { value.take().filter(|value| !value.trim().is_empty()) } pub fn trim_ai_owned_non_empty_string(value: String) -> Option { let trimmed = value.trim(); if trimmed.is_empty() { return None; } if trimmed.len() == value.len() { return Some(value); } Some(trimmed.to_owned()) } pub fn take_ai_decision_plan_core(payload: &mut AiExecutionDecision) -> Option { Some(AiDecisionPlanCore { request_id: take_ai_non_empty_string(&mut payload.request_id)?, provider_id: take_ai_non_empty_string(&mut payload.provider_id)?, endpoint_id: take_ai_non_empty_string(&mut payload.endpoint_id)?, key_id: take_ai_non_empty_string(&mut payload.key_id)?, provider_api_format: take_ai_non_empty_string(&mut payload.provider_api_format)?, client_api_format: take_ai_non_empty_string(&mut payload.client_api_format)?, }) } pub fn take_ai_upstream_auth_pair( payload: &mut AiExecutionDecision, ) -> Option> { let header = take_ai_non_empty_string(&mut payload.auth_header); let value = take_ai_non_empty_string(&mut payload.auth_value); match (header, value) { (Some(header), Some(value)) => Some(Some(AiUpstreamAuthPair { header, value })), (None, None) => Some(None), _ => None, } } pub fn resolve_ai_passthrough_sync_request_body( provider_request_body: Option, provider_request_body_base64: Option, ) -> RequestBody { if let Some(body_bytes_b64) = provider_request_body_base64.and_then(trim_ai_owned_non_empty_string) { return RequestBody { json_body: None, body_bytes_b64: Some(body_bytes_b64), body_ref: None, }; } match provider_request_body.unwrap_or(serde_json::Value::Null) { serde_json::Value::Null => RequestBody { json_body: None, body_bytes_b64: None, body_ref: None, }, other => RequestBody::from_json(other), } } pub fn build_ai_execution_plan_from_decision( payload: &mut AiExecutionDecision, parts: AiExecutionPlanFromDecisionParts, ) -> ExecutionPlan { let explicit_content_encoding = take_ai_non_empty_string(&mut payload.content_encoding); let request_gzip = payload.request_gzip.take(); let content_encoding = explicit_content_encoding .or_else(|| infer_ai_execution_plan_content_encoding(&parts, request_gzip.as_ref())); ExecutionPlan { request_id: parts.core.request_id, candidate_id: payload.candidate_id.take(), provider_name: payload.provider_name.take(), provider_id: parts.core.provider_id, endpoint_id: parts.core.endpoint_id, key_id: parts.core.key_id, method: parts.method, url: parts.url, headers: parts.headers, content_type: parts.content_type, content_encoding, body: parts.body, stream: parts.stream, client_api_format: parts.core.client_api_format, provider_api_format: parts.core.provider_api_format, model_name: payload.model_name.take(), proxy: payload.proxy.take(), transport_profile: payload.transport_profile.take(), timeouts: payload.timeouts.take(), } } fn infer_ai_execution_plan_content_encoding( parts: &AiExecutionPlanFromDecisionParts, request_gzip: Option<&AiRequestGzipPolicy>, ) -> Option { if let Some(should_gzip) = should_gzip_explicit_json_request(parts, request_gzip) { return should_gzip.then(|| "gzip".to_string()); } None } fn should_gzip_explicit_json_request( parts: &AiExecutionPlanFromDecisionParts, request_gzip: Option<&AiRequestGzipPolicy>, ) -> Option { let request_gzip = request_gzip?; let enabled = request_gzip .enabled .unwrap_or(request_gzip.min_bytes.is_some()); if !enabled { return Some(false); } Some(json_request_body_len_at_least( parts, request_gzip .min_bytes .unwrap_or(DEFAULT_REQUEST_GZIP_MIN_JSON_BYTES), )) } fn json_request_body_len_at_least( parts: &AiExecutionPlanFromDecisionParts, min_bytes: usize, ) -> bool { if parts.body.body_bytes_b64.is_some() || parts.body.body_ref.is_some() { return false; } let Some(json_body) = parts.body.json_body.as_ref() else { return false; }; serde_json::to_vec(json_body) .map(|body| body.len() >= min_bytes) .unwrap_or(false) } pub fn build_ai_execution_decision_from_plan( parts: AiExecutionDecisionFromPlanParts, ) -> AiExecutionDecision { let ExecutionPlan { request_id, candidate_id, provider_name, provider_id, endpoint_id, key_id, method, url, headers, content_type, content_encoding, body, stream, client_api_format, provider_api_format, model_name, proxy, transport_profile, timeouts, } = parts.plan; let auth_pair = parts .include_auth_pair .then(|| extract_ai_auth_header_pair(&headers)) .flatten(); let provider_contract = provider_api_format.clone(); let client_contract = client_api_format.clone(); let request_id = parts.request_id.unwrap_or(request_id); let auth_header = auth_pair.map(|(name, _)| name.to_string()); let auth_value = auth_pair.map(|(_, value)| value.to_string()); let RequestBody { json_body, body_bytes_b64, body_ref: _body_ref, } = body; AiExecutionDecision { action: parts.action, decision_kind: parts.decision_kind, execution_strategy: Some(ai_execution_strategy_for_formats( provider_api_format.as_str(), client_api_format.as_str(), )), conversion_mode: Some(ai_conversion_mode_for_formats( provider_api_format.as_str(), client_api_format.as_str(), )), request_id: Some(request_id), candidate_id, provider_name, provider_type: None, provider_id: Some(provider_id), endpoint_id: Some(endpoint_id), key_id: Some(key_id), upstream_base_url: parts.upstream_base_url, upstream_url: Some(url), provider_request_method: Some(method), auth_header, auth_value, provider_api_format: Some(provider_api_format), client_api_format: Some(client_api_format), provider_contract: Some(provider_contract), client_contract: Some(client_contract), model_name, mapped_model: None, prompt_cache_key: None, extra_headers: BTreeMap::new(), provider_request_headers: headers, provider_request_body: json_body, provider_request_body_base64: body_bytes_b64, content_type, content_encoding, request_gzip: None, proxy, transport_profile, timeouts, upstream_is_stream: stream, report_kind: parts.report_kind, report_context: parts.report_context, auth_context: parts.auth_context, } } pub fn extract_ai_auth_header_pair(headers: &BTreeMap) -> Option<(&str, &str)> { [ "authorization", "x-api-key", "api-key", "x-goog-api-key", "proxy-authorization", ] .into_iter() .find_map(|name| { headers .iter() .find(|(header_name, _)| header_name.eq_ignore_ascii_case(name)) .map(|(header_name, value)| (header_name.as_str(), value.as_str())) }) } pub fn infer_ai_upstream_base_url(upstream_url: &str) -> Option { let parsed = Url::parse(upstream_url).ok()?; let host = parsed.host_str()?; let mut base = format!("{}://{}", parsed.scheme(), host); if let Some(port) = parsed.port() { base.push(':'); base.push_str(port.to_string().as_str()); } let base_path = infer_ai_upstream_base_path(parsed.path()); if !base_path.is_empty() { base.push_str(base_path); } Some(base) } fn infer_ai_upstream_base_path(path: &str) -> &str { let trimmed = path.trim_end_matches('/'); if trimmed.is_empty() || trimmed == "/" { return ""; } for suffix in [ "/responses/compact", "/responses", "/chat/completions", "/messages", ] { if let Some(prefix) = trimmed.strip_suffix(suffix) { return normalize_inferred_ai_base_path(prefix); } } for marker in ["/v1/videos", "/v1beta/"] { if let Some((prefix, _)) = trimmed.split_once(marker) { return normalize_inferred_ai_base_path(prefix); } } normalize_inferred_ai_base_path(trimmed) } fn normalize_inferred_ai_base_path(path: &str) -> &str { let trimmed = path.trim_end_matches('/'); if trimmed.is_empty() || trimmed == "/" { "" } else { trimmed } } fn ai_execution_strategy_for_formats(provider_api_format: &str, client_api_format: &str) -> String { if provider_api_format == client_api_format { "local_same_format" } else { "local_cross_format" } .to_string() } fn ai_conversion_mode_for_formats(provider_api_format: &str, client_api_format: &str) -> String { if provider_api_format == client_api_format { "none" } else { "bidirectional" } .to_string() } #[cfg(test)] mod tests { use std::collections::BTreeMap; use serde_json::json; use super::*; #[test] fn take_ai_decision_plan_core_consumes_required_non_empty_fields() { let mut payload = test_decision(); let core = take_ai_decision_plan_core(&mut payload).expect("core fields should be available"); assert_eq!(core.request_id, "req_1"); assert_eq!(core.provider_id, "provider_1"); assert_eq!(core.endpoint_id, "endpoint_1"); assert_eq!(core.key_id, "key_1"); assert_eq!(core.provider_api_format, "openai:chat"); assert_eq!(core.client_api_format, "openai:chat"); assert!(payload.request_id.is_none()); assert!(payload.provider_api_format.is_none()); } #[test] fn take_ai_decision_plan_core_rejects_blank_required_fields() { let mut payload = test_decision(); payload.endpoint_id = Some(" ".to_string()); assert!(take_ai_decision_plan_core(&mut payload).is_none()); } #[test] fn take_ai_upstream_auth_pair_rejects_incomplete_auth() { let mut payload = test_decision(); payload.auth_header = Some("authorization".to_string()); payload.auth_value = Some(" ".to_string()); assert!(take_ai_upstream_auth_pair(&mut payload).is_none()); } #[test] fn resolve_ai_passthrough_sync_request_body_prefers_trimmed_base64() { let body = resolve_ai_passthrough_sync_request_body( Some(json!({"ignored": true})), Some(" YWJj ".to_string()), ); assert_eq!(body.body_bytes_b64.as_deref(), Some("YWJj")); assert!(body.json_body.is_none()); } #[test] fn resolve_ai_passthrough_sync_request_body_uses_json_when_no_base64() { let body = resolve_ai_passthrough_sync_request_body(Some(json!({"ok": true})), None); assert_eq!(body.json_body, Some(json!({"ok": true}))); assert!(body.body_bytes_b64.is_none()); } #[test] fn build_ai_execution_plan_from_decision_merges_core_and_remaining_payload_fields() { let mut payload = test_decision(); let core = take_ai_decision_plan_core(&mut payload).expect("core fields should be available"); let plan = build_ai_execution_plan_from_decision( &mut payload, AiExecutionPlanFromDecisionParts { core, method: "POST".to_string(), url: "https://example.com/v1/chat/completions".to_string(), headers: BTreeMap::from([( "content-type".to_string(), "application/json".to_string(), )]), content_type: Some("application/json".to_string()), body: RequestBody::from_json(json!({"model": "gpt-test"})), stream: true, }, ); assert_eq!(plan.request_id, "req_1"); assert_eq!(plan.candidate_id.as_deref(), Some("candidate_1")); assert_eq!(plan.provider_id, "provider_1"); assert_eq!(plan.endpoint_id, "endpoint_1"); assert_eq!(plan.key_id, "key_1"); assert!(plan.stream); assert_eq!(plan.provider_api_format, "openai:chat"); assert_eq!(plan.client_api_format, "openai:chat"); assert_eq!(plan.model_name.as_deref(), Some("gpt-test")); assert!(payload.candidate_id.is_none()); assert!(payload.model_name.is_none()); } #[test] fn build_ai_execution_plan_without_request_gzip_policy_leaves_json_uncompressed() { let large_codex_url = test_plan_for_url_and_body( "https://chatgpt.com/backend-api/codex/responses", RequestBody::from_json(json!({ "model": "gpt-5.5", "input": "x".repeat(DEFAULT_REQUEST_GZIP_MIN_JSON_BYTES), })), ); let large_openai = test_plan_for_url_and_body( "https://api.openai.com/v1/responses", RequestBody::from_json(json!({ "model": "gpt-5.5", "input": "x".repeat(DEFAULT_REQUEST_GZIP_MIN_JSON_BYTES), })), ); assert_eq!(large_codex_url.content_encoding, None); assert_eq!(large_openai.content_encoding, None); } #[test] fn build_ai_execution_plan_does_not_gzip_raw_body_even_when_explicit() { let mut payload = test_decision(); payload.request_gzip = Some(AiRequestGzipPolicy { enabled: Some(true), min_bytes: Some(0), }); let core = take_ai_decision_plan_core(&mut payload).expect("core fields should be available"); let plan = build_ai_execution_plan_from_decision( &mut payload, AiExecutionPlanFromDecisionParts { core, method: "POST".to_string(), url: "https://api.example.com/v1/chat/completions".to_string(), headers: BTreeMap::new(), content_type: Some("application/json".to_string()), body: RequestBody { json_body: None, body_bytes_b64: Some("dGVzdA==".to_string()), body_ref: None, }, stream: false, }, ); assert_eq!(plan.content_encoding, None); } #[test] fn build_ai_execution_plan_preserves_explicit_content_encoding_for_raw_body() { let mut payload = test_decision(); payload.content_encoding = Some("gzip".to_string()); payload.request_gzip = Some(AiRequestGzipPolicy { enabled: Some(false), min_bytes: None, }); let core = take_ai_decision_plan_core(&mut payload).expect("core fields should be available"); let plan = build_ai_execution_plan_from_decision( &mut payload, AiExecutionPlanFromDecisionParts { core, method: "POST".to_string(), url: "https://api.example.com/v1/chat/completions".to_string(), headers: BTreeMap::new(), content_type: Some("application/octet-stream".to_string()), body: RequestBody { json_body: None, body_bytes_b64: Some("dGVzdA==".to_string()), body_ref: None, }, stream: false, }, ); assert_eq!(plan.content_encoding.as_deref(), Some("gzip")); } #[test] fn build_ai_execution_plan_gzips_explicit_json_request_for_non_codex() { let mut payload = test_decision(); payload.request_gzip = Some(AiRequestGzipPolicy { enabled: Some(true), min_bytes: Some(1), }); let core = take_ai_decision_plan_core(&mut payload).expect("core fields should be available"); let plan = build_ai_execution_plan_from_decision( &mut payload, AiExecutionPlanFromDecisionParts { core, method: "POST".to_string(), url: "https://api.example.com/v1/chat/completions".to_string(), headers: BTreeMap::new(), content_type: Some("application/json".to_string()), body: RequestBody::from_json(json!({"model": "gpt-test"})), stream: false, }, ); assert_eq!(plan.content_encoding.as_deref(), Some("gzip")); } #[test] fn build_ai_execution_plan_respects_explicit_request_gzip_threshold() { let mut payload = test_decision(); payload.request_gzip = Some(AiRequestGzipPolicy { enabled: Some(true), min_bytes: Some(1024), }); let core = take_ai_decision_plan_core(&mut payload).expect("core fields should be available"); let plan = build_ai_execution_plan_from_decision( &mut payload, AiExecutionPlanFromDecisionParts { core, method: "POST".to_string(), url: "https://api.example.com/v1/chat/completions".to_string(), headers: BTreeMap::new(), content_type: Some("application/json".to_string()), body: RequestBody::from_json(json!({"model": "gpt-test"})), stream: false, }, ); assert_eq!(plan.content_encoding, None); } #[test] fn build_ai_execution_plan_explicit_request_gzip_false_disables_gzip() { let mut payload = test_decision(); payload.provider_api_format = Some("openai:responses".to_string()); payload.client_api_format = Some("openai:responses".to_string()); payload.request_gzip = Some(AiRequestGzipPolicy { enabled: Some(false), min_bytes: None, }); let core = take_ai_decision_plan_core(&mut payload).expect("core fields should be available"); let plan = build_ai_execution_plan_from_decision( &mut payload, AiExecutionPlanFromDecisionParts { core, method: "POST".to_string(), url: "https://chatgpt.com/backend-api/codex/responses".to_string(), headers: BTreeMap::new(), content_type: Some("application/json".to_string()), body: RequestBody::from_json(json!({ "model": "gpt-5.5", "input": "x".repeat(DEFAULT_REQUEST_GZIP_MIN_JSON_BYTES), })), stream: true, }, ); assert_eq!(plan.content_encoding, None); } #[test] fn infer_ai_upstream_base_url_preserves_codex_base_path() { assert_eq!( infer_ai_upstream_base_url("https://tiger.bookapi.cc/codex/responses").as_deref(), Some("https://tiger.bookapi.cc/codex") ); assert_eq!( infer_ai_upstream_base_url("https://chatgpt.com/backend-api/codex/responses") .as_deref(), Some("https://chatgpt.com/backend-api/codex") ); } #[test] fn infer_ai_upstream_base_url_preserves_nested_v1_prefix() { assert_eq!( infer_ai_upstream_base_url( "https://api.openai.example/custom/v1/chat/completions?mode=1" ) .as_deref(), Some("https://api.openai.example/custom/v1") ); } #[test] fn infer_ai_upstream_base_url_strips_video_operation_path() { assert_eq!( infer_ai_upstream_base_url("https://video.example/nested/v1/videos/task-123/content") .as_deref(), Some("https://video.example/nested") ); } #[test] fn build_ai_execution_decision_from_plan_maps_plan_fields() { let plan = ExecutionPlan { request_id: "plan-request".to_string(), candidate_id: Some("candidate-1".to_string()), provider_name: Some("provider".to_string()), provider_id: "provider-1".to_string(), endpoint_id: "endpoint-1".to_string(), key_id: "key-1".to_string(), method: "POST".to_string(), url: "https://api.example.com/v1/chat/completions".to_string(), headers: BTreeMap::from([("Authorization".to_string(), "Bearer secret".to_string())]), content_type: Some("application/json".to_string()), content_encoding: None, body: RequestBody::from_json(json!({"model": "mapped"})), stream: false, client_api_format: "openai:chat".to_string(), provider_api_format: "claude:messages".to_string(), model_name: Some("mapped".to_string()), proxy: None, transport_profile: None, timeouts: None, }; let decision = build_ai_execution_decision_from_plan(AiExecutionDecisionFromPlanParts { action: "execution_runtime.sync_decision".to_string(), decision_kind: Some("openai_chat_sync".to_string()), request_id: Some("trace-1".to_string()), upstream_base_url: Some("https://api.example.com".to_string()), include_auth_pair: true, plan, report_kind: Some("report".to_string()), report_context: Some(json!({"candidate_index": 0})), auth_context: None, }); assert_eq!(decision.request_id.as_deref(), Some("trace-1")); assert_eq!( decision.execution_strategy.as_deref(), Some("local_cross_format") ); assert_eq!(decision.conversion_mode.as_deref(), Some("bidirectional")); assert_eq!(decision.auth_header.as_deref(), Some("Authorization")); assert_eq!(decision.auth_value.as_deref(), Some("Bearer secret")); assert_eq!( decision.provider_request_body, Some(json!({"model": "mapped"})) ); assert_eq!(decision.report_kind.as_deref(), Some("report")); } #[test] fn plan_decision_round_trip_preserves_raw_body_content_encoding() { let original = ExecutionPlan { request_id: "plan-request".to_string(), candidate_id: Some("candidate-1".to_string()), provider_name: Some("provider".to_string()), provider_id: "provider-1".to_string(), endpoint_id: "endpoint-1".to_string(), key_id: "key-1".to_string(), method: "POST".to_string(), url: "https://api.example.com/v1/upload".to_string(), headers: BTreeMap::from([( "content-type".to_string(), "application/octet-stream".to_string(), )]), content_type: Some("application/octet-stream".to_string()), content_encoding: Some("gzip".to_string()), body: RequestBody { json_body: None, body_bytes_b64: Some("dGVzdA==".to_string()), body_ref: None, }, stream: false, client_api_format: "openai:chat".to_string(), provider_api_format: "openai:chat".to_string(), model_name: Some("gpt-test".to_string()), proxy: None, transport_profile: None, timeouts: None, }; let mut decision = build_ai_execution_decision_from_plan(AiExecutionDecisionFromPlanParts { action: "execution_runtime.sync_decision".to_string(), decision_kind: Some("raw_upload_sync".to_string()), request_id: None, upstream_base_url: Some("https://api.example.com".to_string()), include_auth_pair: false, plan: original, report_kind: None, report_context: None, auth_context: None, }); assert_eq!(decision.content_encoding.as_deref(), Some("gzip")); assert!(decision.request_gzip.is_none()); let core = take_ai_decision_plan_core(&mut decision).expect("core fields should be available"); let method = take_ai_non_empty_string(&mut decision.provider_request_method) .expect("method should round-trip"); let url = take_ai_non_empty_string(&mut decision.upstream_url).expect("url should round-trip"); let headers = std::mem::take(&mut decision.provider_request_headers); let content_type = decision.content_type.take(); let body = resolve_ai_passthrough_sync_request_body( decision.provider_request_body.take(), decision.provider_request_body_base64.take(), ); let stream = decision.upstream_is_stream; let round_tripped = build_ai_execution_plan_from_decision( &mut decision, AiExecutionPlanFromDecisionParts { core, method, url, headers, content_type, body, stream, }, ); assert_eq!(round_tripped.content_encoding.as_deref(), Some("gzip")); assert_eq!( round_tripped.body.body_bytes_b64.as_deref(), Some("dGVzdA==") ); assert!(round_tripped.body.json_body.is_none()); } fn test_decision() -> AiExecutionDecision { AiExecutionDecision { action: "sync".to_string(), decision_kind: Some("test".to_string()), execution_strategy: None, conversion_mode: None, request_id: Some("req_1".to_string()), candidate_id: Some("candidate_1".to_string()), provider_name: Some("provider".to_string()), provider_type: None, provider_id: Some("provider_1".to_string()), endpoint_id: Some("endpoint_1".to_string()), key_id: Some("key_1".to_string()), upstream_base_url: Some("https://example.com".to_string()), upstream_url: Some("https://example.com/v1/chat/completions".to_string()), provider_request_method: None, auth_header: Some("authorization".to_string()), auth_value: Some("Bearer token".to_string()), provider_api_format: Some("openai:chat".to_string()), client_api_format: Some("openai:chat".to_string()), provider_contract: Some("openai:chat".to_string()), client_contract: Some("openai:chat".to_string()), model_name: Some("gpt-test".to_string()), mapped_model: Some("gpt-test".to_string()), prompt_cache_key: None, extra_headers: BTreeMap::new(), provider_request_headers: BTreeMap::new(), provider_request_body: None, provider_request_body_base64: None, content_type: None, content_encoding: None, request_gzip: None, proxy: None, transport_profile: None, timeouts: None, upstream_is_stream: false, report_kind: None, report_context: None, auth_context: None, } } fn test_plan_for_url_and_body(url: &str, body: RequestBody) -> ExecutionPlan { test_plan_for_url_body_and_format(url, body, "openai:responses") } fn test_plan_for_url_body_and_format( url: &str, body: RequestBody, provider_api_format: &str, ) -> ExecutionPlan { let mut payload = test_decision(); payload.provider_api_format = Some(provider_api_format.to_string()); payload.client_api_format = Some(provider_api_format.to_string()); let core = take_ai_decision_plan_core(&mut payload).expect("core fields should be available"); build_ai_execution_plan_from_decision( &mut payload, AiExecutionPlanFromDecisionParts { core, method: "POST".to_string(), url: url.to_string(), headers: BTreeMap::from([( "content-type".to_string(), "application/json".to_string(), )]), content_type: Some("application/json".to_string()), body, stream: true, }, ) } }