feat(codex): add provider outbound policy boundary

This commit is contained in:
elky
2026-09-01 21:21:42 +08:00
parent 715f2773c3
commit d5f34b2ee2
12 changed files with 1313 additions and 198 deletions
@@ -5,72 +5,17 @@ use serde_json::{Map, Value};
use sha2::{Digest, Sha256};
use uuid::Uuid;
use crate::outbound_request_policy::{
ProviderOutboundRequestContext, ProviderOutboundRequestIdentityScope,
ProviderOutboundRequestMutationScope, ProviderOutboundRequestPolicy,
ProviderOutboundRequestPolicyReason, ProviderOutboundRequestPolicyResult,
};
use crate::snapshot::GatewayProviderTransportSnapshot;
pub const CODEX_FINGERPRINT_CONFIG_NAMESPACE: &str = "codex";
pub const CODEX_FINGERPRINT_ENABLED_CONFIG_KEY: &str = "fingerprint_convergence_enabled";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CodexFingerprintConvergenceContext {
logical_turn_id: String,
original_turn_id: Option<String>,
original_client_session_id: Option<String>,
original_prompt_cache_key: Option<String>,
turn_started_at_unix_ms: u64,
}
impl CodexFingerprintConvergenceContext {
pub fn new(logical_turn_id: impl Into<String>, turn_started_at_unix_ms: u64) -> Self {
Self {
logical_turn_id: logical_turn_id.into().trim().to_string(),
original_turn_id: None,
original_client_session_id: None,
original_prompt_cache_key: None,
turn_started_at_unix_ms,
}
}
pub fn with_original_turn_id(mut self, original_turn_id: impl Into<String>) -> Self {
self.original_turn_id = non_empty_owned(original_turn_id.into());
self
}
pub fn with_original_client_session_id(
mut self,
original_client_session_id: impl Into<String>,
) -> Self {
self.original_client_session_id = non_empty_owned(original_client_session_id.into());
self
}
pub fn with_original_prompt_cache_key(
mut self,
original_prompt_cache_key: impl Into<String>,
) -> Self {
self.original_prompt_cache_key = non_empty_owned(original_prompt_cache_key.into());
self
}
pub fn logical_turn_id(&self) -> &str {
self.logical_turn_id.as_str()
}
pub fn original_turn_id(&self) -> Option<&str> {
self.original_turn_id.as_deref()
}
pub fn original_client_session_id(&self) -> Option<&str> {
self.original_client_session_id.as_deref()
}
pub fn original_prompt_cache_key(&self) -> Option<&str> {
self.original_prompt_cache_key.as_deref()
}
pub fn turn_started_at_unix_ms(&self) -> u64 {
self.turn_started_at_unix_ms
}
}
pub type CodexFingerprintConvergenceContext = ProviderOutboundRequestContext;
#[derive(Debug, Clone, PartialEq, Eq)]
struct CodexConvergedFingerprint {
@@ -125,36 +70,82 @@ pub fn apply_codex_fingerprint_convergence_with_context(
provider_request_headers: &mut BTreeMap<String, String>,
provider_request_body: &mut Value,
) -> bool {
let is_responses = aether_ai_formats::is_openai_responses_format(provider_api_format);
let is_live = aether_ai_formats::api_format_alias_matches(provider_api_format, "codex:live");
// Convergence is a Codex provider policy, independent of whether the key
// uses OAuth, an API key, or another ordinary auth channel. Agent Identity
// uses a separate signed-identity protocol and is excluded here.
apply_codex_fingerprint_convergence_policy(
transport,
provider_api_format,
context,
provider_request_headers,
provider_request_body,
)
.was_applied()
}
pub(crate) fn apply_codex_fingerprint_convergence_policy(
transport: &GatewayProviderTransportSnapshot,
provider_api_format: &str,
context: &ProviderOutboundRequestContext,
provider_request_headers: &mut BTreeMap<String, String>,
provider_request_body: &mut Value,
) -> ProviderOutboundRequestPolicyResult {
let policy = ProviderOutboundRequestPolicy::CodexFingerprintConvergence;
if !transport
.provider
.provider_type
.trim()
.eq_ignore_ascii_case("codex")
|| crate::agent_identity::is_codex_agent_identity_transport(transport)
|| (!is_responses && !is_live)
|| is_responses
&& aether_ai_formats::openai_responses_request_operation(
provider_api_format,
provider_request_body,
) == Some(aether_ai_formats::OPENAI_RESPONSES_OPERATION_COMPACT)
|| !codex_fingerprint_convergence_enabled(
transport.provider.provider_type.as_str(),
transport.provider.config.as_ref(),
)
|| !provider_request_body.is_object()
{
return false;
return ProviderOutboundRequestPolicyResult::skipped(
policy,
ProviderOutboundRequestPolicyReason::ProviderTypeMismatch,
);
}
if crate::agent_identity::is_codex_agent_identity_transport(transport) {
return ProviderOutboundRequestPolicyResult::skipped(
policy,
ProviderOutboundRequestPolicyReason::AgentIdentityExcluded,
);
}
let is_responses = aether_ai_formats::is_openai_responses_format(provider_api_format);
let is_live = aether_ai_formats::api_format_alias_matches(provider_api_format, "codex:live");
if !is_responses && !is_live {
return ProviderOutboundRequestPolicyResult::skipped(
policy,
ProviderOutboundRequestPolicyReason::UnsupportedApiFormat,
);
}
if is_responses
&& aether_ai_formats::openai_responses_request_operation(
provider_api_format,
provider_request_body,
) == Some(aether_ai_formats::OPENAI_RESPONSES_OPERATION_COMPACT)
{
return ProviderOutboundRequestPolicyResult::skipped(
policy,
ProviderOutboundRequestPolicyReason::CompactOperationExcluded,
);
}
if !codex_fingerprint_convergence_enabled(
transport.provider.provider_type.as_str(),
transport.provider.config.as_ref(),
) {
return ProviderOutboundRequestPolicyResult::skipped(
policy,
ProviderOutboundRequestPolicyReason::Disabled,
);
}
if !provider_request_body.is_object() {
return ProviderOutboundRequestPolicyResult::skipped(
policy,
ProviderOutboundRequestPolicyReason::RequestBodyNotObject,
);
}
let auth_identity = aether_ai_formats::parse_codex_auth_identity(
transport.key.decrypted_auth_config.as_deref(),
);
let account_seed = resolve_codex_account_seed(&auth_identity, transport.key.id.as_str());
let (account_seed, identity_scope) =
resolve_codex_account_seed_with_scope(&auth_identity, transport.key.id.as_str());
// Only namespace a cache key that survived all provider-body conversion and
// routing rules. The client-side value in `context` is a retry signal, not
// permission to resurrect a field that the terminal body deliberately
@@ -178,7 +169,15 @@ pub fn apply_codex_fingerprint_convergence_with_context(
if is_responses {
apply_converged_client_metadata(provider_request_body, &fingerprint);
}
true
ProviderOutboundRequestPolicyResult::applied(
policy,
if is_responses {
ProviderOutboundRequestMutationScope::HeadersAndBody
} else {
ProviderOutboundRequestMutationScope::Headers
},
identity_scope,
)
}
#[cfg(test)]
@@ -242,11 +241,6 @@ fn resolve_converged_fingerprint_with_prompt_cache(
}
}
fn non_empty_owned(value: String) -> Option<String> {
let value = value.trim();
(!value.is_empty()).then(|| value.to_string())
}
fn current_unix_millis() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
@@ -261,32 +255,62 @@ fn normalized_identity_part(value: Option<&str>) -> Option<String> {
.map(str::to_ascii_lowercase)
}
#[cfg(test)]
fn resolve_codex_account_seed(
identity: &aether_ai_formats::CodexAuthIdentity,
fallback_key_id: &str,
) -> String {
let fingerprint = normalized_identity_part(identity.codex_identity_fingerprint.as_deref())
.or_else(|| {
aether_oauth::provider::providers::derive_codex_identity_fingerprint(
identity.account_id.as_deref(),
identity.account_user_id.as_deref(),
identity.user_id.as_deref(),
identity.email.as_deref(),
)
});
if let Some(fingerprint) = fingerprint {
return format!("persisted:v1:{fingerprint}");
resolve_codex_account_seed_with_scope(identity, fallback_key_id).0
}
fn resolve_codex_account_seed_with_scope(
identity: &aether_ai_formats::CodexAuthIdentity,
fallback_key_id: &str,
) -> (String, ProviderOutboundRequestIdentityScope) {
if let Some(fingerprint) =
normalized_identity_part(identity.codex_identity_fingerprint.as_deref())
{
return (
format!("persisted:v1:{fingerprint}"),
ProviderOutboundRequestIdentityScope::PersistedFingerprint,
);
}
let account = normalized_identity_part(identity.account_id.as_deref());
let member = normalized_identity_part(identity.account_user_id.as_deref())
.or_else(|| normalized_identity_part(identity.user_id.as_deref()))
.or_else(|| normalized_identity_part(identity.email.as_deref()));
if let Some(fingerprint) = aether_oauth::provider::providers::derive_codex_identity_fingerprint(
account.as_deref(),
member.as_deref(),
None,
None,
) {
let scope = if account.is_some() {
ProviderOutboundRequestIdentityScope::AccountMember
} else {
ProviderOutboundRequestIdentityScope::Member
};
return (format!("persisted:v1:{fingerprint}"), scope);
}
match (account, member) {
(Some(account), Some(member)) => format!("account-member:v1:{account}\0{member}"),
(None, Some(member)) => format!("member:v1:{member}"),
(Some(account), None) => format!("account:v1:{account}"),
(None, None) => format!("key:v1:{}", fallback_key_id.trim()),
(Some(account), Some(member)) => (
format!("account-member:v1:{account}\0{member}"),
ProviderOutboundRequestIdentityScope::AccountMember,
),
(None, Some(member)) => (
format!("member:v1:{member}"),
ProviderOutboundRequestIdentityScope::Member,
),
(Some(account), None) => (
format!("account:v1:{account}"),
ProviderOutboundRequestIdentityScope::Account,
),
(None, None) => (
format!("key:v1:{}", fallback_key_id.trim()),
ProviderOutboundRequestIdentityScope::Key,
),
}
}
@@ -1201,5 +1225,131 @@ mod tests {
));
assert_eq!(headers, original_headers);
assert_eq!(body, original_body);
let context = ProviderOutboundRequestContext::new("logical-turn", 1_700_000_000_123);
let result = apply_codex_fingerprint_convergence_policy(
&transport,
"openai:responses",
&context,
&mut headers,
&mut body,
);
assert_eq!(
result.reason,
ProviderOutboundRequestPolicyReason::AgentIdentityExcluded
);
assert_eq!(headers, original_headers);
assert_eq!(body, original_body);
}
#[test]
fn policy_result_reports_applied_scopes_without_identity_values() {
let transport = sample_transport();
let context = ProviderOutboundRequestContext::new("logical-turn", 1_700_000_000_123);
let mut headers = BTreeMap::new();
let mut body = json!({"model": "gpt-5.4"});
let result = apply_codex_fingerprint_convergence_policy(
&transport,
"openai:responses",
&context,
&mut headers,
&mut body,
);
assert_eq!(
result,
ProviderOutboundRequestPolicyResult {
policy: ProviderOutboundRequestPolicy::CodexFingerprintConvergence,
outcome:
crate::outbound_request_policy::ProviderOutboundRequestPolicyOutcome::Applied,
reason: ProviderOutboundRequestPolicyReason::Applied,
mutation_scope: Some(ProviderOutboundRequestMutationScope::HeadersAndBody),
identity_scope: Some(ProviderOutboundRequestIdentityScope::Account),
}
);
let serialized = serde_json::to_value(result).expect("serialize policy result");
let serialized = serialized.as_object().expect("policy result object");
assert_eq!(serialized.len(), 5);
assert!(!serialized.contains_key("installation_id"));
assert!(!serialized.contains_key("session_id"));
assert!(!serialized.contains_key("turn_id"));
}
#[test]
fn policy_result_distinguishes_codex_skip_reasons_without_mutation() {
let context = ProviderOutboundRequestContext::new("logical-turn", 1_700_000_000_123);
let original_headers = BTreeMap::from([("x-custom".to_string(), "preserve".to_string())]);
let original_body = json!({"model": "gpt-5.4"});
let cases = [
(
"provider_type_mismatch",
"openai",
Some(json!({"codex": {"fingerprint_convergence_enabled": true}})),
"openai:responses",
original_body.clone(),
ProviderOutboundRequestPolicyReason::ProviderTypeMismatch,
),
(
"unsupported_api_format",
"codex",
Some(json!({"codex": {"fingerprint_convergence_enabled": true}})),
"openai:chat",
original_body.clone(),
ProviderOutboundRequestPolicyReason::UnsupportedApiFormat,
),
(
"compact_operation",
"codex",
Some(json!({"codex": {"fingerprint_convergence_enabled": true}})),
"openai:responses",
json!({"model": "gpt-5.4", "input": [{"type": "compaction_trigger"}]}),
ProviderOutboundRequestPolicyReason::CompactOperationExcluded,
),
(
"disabled",
"codex",
None,
"openai:responses",
original_body.clone(),
ProviderOutboundRequestPolicyReason::Disabled,
),
(
"request_body_not_object",
"codex",
Some(json!({"codex": {"fingerprint_convergence_enabled": true}})),
"openai:responses",
json!(["not-an-object"]),
ProviderOutboundRequestPolicyReason::RequestBodyNotObject,
),
];
for (name, provider_type, config, api_format, body, expected_reason) in cases {
let mut transport = sample_transport();
transport.provider.provider_type = provider_type.to_string();
transport.provider.config = config;
let mut headers = original_headers.clone();
let mut request_body = body.clone();
let result = apply_codex_fingerprint_convergence_policy(
&transport,
api_format,
&context,
&mut headers,
&mut request_body,
);
assert_eq!(
result.outcome,
crate::outbound_request_policy::ProviderOutboundRequestPolicyOutcome::Skipped,
"case={name}"
);
assert_eq!(result.reason, expected_reason, "case={name}");
assert_eq!(result.mutation_scope, None, "case={name}");
assert_eq!(result.identity_scope, None, "case={name}");
assert_eq!(headers, original_headers, "case={name}");
assert_eq!(request_body, body, "case={name}");
}
}
}
@@ -17,6 +17,7 @@ pub mod kiro;
mod network;
pub mod oauth_refresh;
mod openai_image;
mod outbound_request_policy;
pub mod policy;
pub mod provider_types;
mod request_body;
@@ -120,6 +121,13 @@ pub use openai_image::{
openai_image_transport_unsupported_reason, resolve_openai_image_auth,
ProviderOpenAiImageHeadersInput,
};
pub use outbound_request_policy::{
apply_provider_outbound_request_policies, ProviderOutboundRequestContext,
ProviderOutboundRequestIdentityScope, ProviderOutboundRequestMutationScope,
ProviderOutboundRequestPolicy, ProviderOutboundRequestPolicyOutcome,
ProviderOutboundRequestPolicyReason, ProviderOutboundRequestPolicyResult,
PROVIDER_OUTBOUND_CONTEXT_MAX_VALUE_BYTES,
};
pub use policy::{
local_gemini_transport_unsupported_reason,
local_gemini_transport_unsupported_reason_with_network,
@@ -0,0 +1,396 @@
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};
use crate::snapshot::GatewayProviderTransportSnapshot;
/// Stable request-level signals that provider-specific outbound policies may use.
///
/// The context deliberately carries client inputs rather than provider-derived
/// identities. Policies remain responsible for deriving and applying their own
/// wire representation at the terminal transport boundary.
const CONTEXT_HASH_DOMAIN: &[u8] = b"aether-provider-outbound-context-v1";
const CONTEXT_HASH_PREFIX: &str = "aether:provider-context:v1:";
/// Maximum byte length of any value retained in a cross-stage provider context.
///
/// The limit is enforced before a context can reach Live persistence. Values
/// above the limit are represented by a deterministic, field-scoped digest so
/// retries keep the same policy identity without allowing unbounded client
/// input into the registry.
pub const PROVIDER_OUTBOUND_CONTEXT_MAX_VALUE_BYTES: usize = 256;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderOutboundRequestContext {
logical_turn_id: String,
original_turn_id: Option<String>,
original_client_session_id: Option<String>,
original_prompt_cache_key: Option<String>,
turn_started_at_unix_ms: u64,
}
impl ProviderOutboundRequestContext {
pub fn new(logical_turn_id: impl Into<String>, turn_started_at_unix_ms: u64) -> Self {
Self {
logical_turn_id: canonical_required_value(logical_turn_id.into(), "logical_turn_id"),
original_turn_id: None,
original_client_session_id: None,
original_prompt_cache_key: None,
turn_started_at_unix_ms,
}
}
pub fn with_original_turn_id(mut self, original_turn_id: impl Into<String>) -> Self {
self.original_turn_id = canonical_optional_value(original_turn_id.into(), "turn_id");
self
}
pub fn with_original_client_session_id(
mut self,
original_client_session_id: impl Into<String>,
) -> Self {
self.original_client_session_id =
canonical_optional_value(original_client_session_id.into(), "client_session_id");
self
}
pub fn with_original_prompt_cache_key(
mut self,
original_prompt_cache_key: impl Into<String>,
) -> Self {
self.original_prompt_cache_key =
canonical_optional_value(original_prompt_cache_key.into(), "prompt_cache_key");
self
}
pub fn logical_turn_id(&self) -> &str {
self.logical_turn_id.as_str()
}
pub fn original_turn_id(&self) -> Option<&str> {
self.original_turn_id.as_deref()
}
pub fn original_client_session_id(&self) -> Option<&str> {
self.original_client_session_id.as_deref()
}
pub fn original_prompt_cache_key(&self) -> Option<&str> {
self.original_prompt_cache_key.as_deref()
}
pub fn turn_started_at_unix_ms(&self) -> u64 {
self.turn_started_at_unix_ms
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProviderOutboundRequestPolicy {
CodexFingerprintConvergence,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProviderOutboundRequestPolicyOutcome {
Applied,
Skipped,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProviderOutboundRequestPolicyReason {
Applied,
ProviderTypeMismatch,
AgentIdentityExcluded,
UnsupportedApiFormat,
CompactOperationExcluded,
Disabled,
RequestBodyNotObject,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProviderOutboundRequestMutationScope {
Headers,
Body,
HeadersAndBody,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProviderOutboundRequestIdentityScope {
PersistedFingerprint,
AccountMember,
Member,
Account,
Key,
}
/// Low-sensitivity report for one selected provider-specific policy.
///
/// This type intentionally contains only categorical values. Derived identity
/// values, client identifiers, and cache keys must never be added to it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProviderOutboundRequestPolicyResult {
pub policy: ProviderOutboundRequestPolicy,
pub outcome: ProviderOutboundRequestPolicyOutcome,
pub reason: ProviderOutboundRequestPolicyReason,
#[serde(skip_serializing_if = "Option::is_none")]
pub mutation_scope: Option<ProviderOutboundRequestMutationScope>,
#[serde(skip_serializing_if = "Option::is_none")]
pub identity_scope: Option<ProviderOutboundRequestIdentityScope>,
}
impl ProviderOutboundRequestPolicyResult {
pub(crate) fn applied(
policy: ProviderOutboundRequestPolicy,
mutation_scope: ProviderOutboundRequestMutationScope,
identity_scope: ProviderOutboundRequestIdentityScope,
) -> Self {
Self {
policy,
outcome: ProviderOutboundRequestPolicyOutcome::Applied,
reason: ProviderOutboundRequestPolicyReason::Applied,
mutation_scope: Some(mutation_scope),
identity_scope: Some(identity_scope),
}
}
pub(crate) fn skipped(
policy: ProviderOutboundRequestPolicy,
reason: ProviderOutboundRequestPolicyReason,
) -> Self {
Self {
policy,
outcome: ProviderOutboundRequestPolicyOutcome::Skipped,
reason,
mutation_scope: None,
identity_scope: None,
}
}
pub fn was_applied(&self) -> bool {
self.outcome == ProviderOutboundRequestPolicyOutcome::Applied
}
}
/// Applies the statically registered outbound policies for the final provider.
///
/// Provider selection has already completed at this boundary. A provider with
/// no registered adapter is a strict no-op and produces no policy result.
pub fn apply_provider_outbound_request_policies(
transport: &GatewayProviderTransportSnapshot,
provider_api_format: &str,
context: &ProviderOutboundRequestContext,
provider_request_headers: &mut BTreeMap<String, String>,
provider_request_body: &mut Value,
) -> Vec<ProviderOutboundRequestPolicyResult> {
if !transport
.provider
.provider_type
.trim()
.eq_ignore_ascii_case("codex")
{
return Vec::new();
}
vec![
crate::codex_fingerprint::apply_codex_fingerprint_convergence_policy(
transport,
provider_api_format,
context,
provider_request_headers,
provider_request_body,
),
]
}
fn canonical_required_value(value: String, field: &str) -> String {
let value = value.trim();
// Bound the JSON-encoded representation, not just the source bytes. This
// preserves ordinary Unicode while preventing quotes, backslashes, or
// control characters from escaping beyond the aggregate Live record
// budget.
if value.len() <= PROVIDER_OUTBOUND_CONTEXT_MAX_VALUE_BYTES
&& serde_json::to_string(value).is_ok_and(|encoded| {
encoded.len() <= PROVIDER_OUTBOUND_CONTEXT_MAX_VALUE_BYTES.saturating_add(2)
})
{
return value.to_string();
}
digest_context_value(field, value)
}
fn canonical_optional_value(value: String, field: &str) -> Option<String> {
let value = value.trim();
(!value.is_empty()).then(|| canonical_required_value(value.to_string(), field))
}
fn digest_context_value(field: &str, value: &str) -> String {
let mut digest = Sha256::new();
digest.update(CONTEXT_HASH_DOMAIN);
digest.update([0]);
digest.update(field.as_bytes());
digest.update([0]);
digest.update((value.len() as u64).to_be_bytes());
digest.update(value.as_bytes());
format!("{CONTEXT_HASH_PREFIX}{field}:{:x}", digest.finalize())
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
use crate::snapshot::{
GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
GatewayProviderTransportProvider,
};
fn sample_transport(provider_type: &str) -> GatewayProviderTransportSnapshot {
GatewayProviderTransportSnapshot {
provider: GatewayProviderTransportProvider {
id: "provider-1".to_string(),
name: "Provider".to_string(),
provider_type: provider_type.to_string(),
website: None,
is_active: true,
keep_priority_on_conversion: false,
enable_format_conversion: true,
concurrent_limit: None,
max_retries: None,
proxy: None,
request_timeout_secs: None,
stream_first_byte_timeout_secs: None,
config: Some(json!({
"codex": {"fingerprint_convergence_enabled": true}
})),
},
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-1".to_string(),
provider_id: "provider-1".to_string(),
api_format: "openai:responses".to_string(),
api_family: None,
endpoint_kind: None,
is_active: true,
base_url: "https://example.com".to_string(),
header_rules: None,
body_rules: None,
max_retries: None,
custom_path: None,
config: None,
format_acceptance_config: None,
proxy: None,
},
key: GatewayProviderTransportKey {
id: "key-1".to_string(),
provider_id: "provider-1".to_string(),
name: "Key".to_string(),
auth_type: "api_key".to_string(),
is_active: true,
api_formats: None,
auth_type_by_format: None,
allow_auth_channel_mismatch_formats: None,
allowed_models: None,
capabilities: None,
rate_multipliers: None,
global_priority_by_format: None,
expires_at_unix_secs: None,
proxy: None,
fingerprint: None,
upstream_metadata: None,
decrypted_api_key: "secret".to_string(),
decrypted_auth_config: None,
},
}
}
#[test]
fn dispatcher_leaves_unregistered_provider_requests_unchanged() {
let transport = sample_transport("openai");
let context = ProviderOutboundRequestContext::new("logical-turn", 1_700_000_000_123);
let original_headers = BTreeMap::from([("x-custom".to_string(), "preserve".to_string())]);
let original_body = json!({"model": "gpt-5.4", "custom": true});
let mut headers = original_headers.clone();
let mut body = original_body.clone();
let results = apply_provider_outbound_request_policies(
&transport,
"openai:responses",
&context,
&mut headers,
&mut body,
);
assert!(results.is_empty());
assert_eq!(headers, original_headers);
assert_eq!(body, original_body);
}
#[test]
fn result_serialization_is_categorical_and_snake_case() {
let result = ProviderOutboundRequestPolicyResult::applied(
ProviderOutboundRequestPolicy::CodexFingerprintConvergence,
ProviderOutboundRequestMutationScope::HeadersAndBody,
ProviderOutboundRequestIdentityScope::AccountMember,
);
assert_eq!(
serde_json::to_value(result).expect("serialize policy result"),
json!({
"policy": "codex_fingerprint_convergence",
"outcome": "applied",
"reason": "applied",
"mutation_scope": "headers_and_body",
"identity_scope": "account_member"
})
);
}
#[test]
fn context_values_are_bounded_and_deterministic() {
let oversized = "x".repeat(PROVIDER_OUTBOUND_CONTEXT_MAX_VALUE_BYTES + 1);
let context = ProviderOutboundRequestContext::new(oversized.clone(), 1)
.with_original_turn_id(oversized.clone())
.with_original_client_session_id(oversized.clone())
.with_original_prompt_cache_key(oversized);
let same_context = ProviderOutboundRequestContext::new(
"x".repeat(PROVIDER_OUTBOUND_CONTEXT_MAX_VALUE_BYTES + 1),
1,
)
.with_original_turn_id("x".repeat(PROVIDER_OUTBOUND_CONTEXT_MAX_VALUE_BYTES + 1))
.with_original_client_session_id("x".repeat(PROVIDER_OUTBOUND_CONTEXT_MAX_VALUE_BYTES + 1))
.with_original_prompt_cache_key("x".repeat(PROVIDER_OUTBOUND_CONTEXT_MAX_VALUE_BYTES + 1));
assert_eq!(context, same_context);
assert!(context.logical_turn_id().len() <= PROVIDER_OUTBOUND_CONTEXT_MAX_VALUE_BYTES);
assert!(context
.original_turn_id()
.is_some_and(|value| value.len() <= PROVIDER_OUTBOUND_CONTEXT_MAX_VALUE_BYTES));
assert!(context
.original_client_session_id()
.is_some_and(|value| value.len() <= PROVIDER_OUTBOUND_CONTEXT_MAX_VALUE_BYTES));
assert!(context
.original_prompt_cache_key()
.is_some_and(|value| value.len() <= PROVIDER_OUTBOUND_CONTEXT_MAX_VALUE_BYTES));
assert!(context.logical_turn_id().starts_with(CONTEXT_HASH_PREFIX));
assert_ne!(
context.logical_turn_id(),
ProviderOutboundRequestContext::new(
"y".repeat(PROVIDER_OUTBOUND_CONTEXT_MAX_VALUE_BYTES + 1),
1,
)
.logical_turn_id()
);
let unicode = ProviderOutboundRequestContext::new("turn-你好", 1);
assert_eq!(unicode.logical_turn_id(), "turn-你好");
let escaped = ProviderOutboundRequestContext::new(
r#"""#.repeat(PROVIDER_OUTBOUND_CONTEXT_MAX_VALUE_BYTES),
1,
);
assert!(escaped.logical_turn_id().starts_with(CONTEXT_HASH_PREFIX));
}
}