Harden PII redaction format conversion

This commit is contained in:
elky
2026-06-14 20:36:57 +08:00
parent 68038c182b
commit 669636d3e4
15 changed files with 813 additions and 285 deletions
@@ -29,7 +29,6 @@ impl<'a> ProviderRequestRedaction<'a> {
#[derive(Clone, Copy, Debug, Default)]
struct ChatPiiRedactionFeatureSettings {
enabled: Option<bool>,
inject_model_instruction: Option<bool>,
}
impl ChatPiiRedactionFeatureSettings {
@@ -44,21 +43,11 @@ impl ChatPiiRedactionFeatureSettings {
if let Some(enabled) = settings.get("enabled").and_then(Value::as_bool) {
self.enabled = Some(enabled);
}
if let Some(inject_model_instruction) = settings
.get("inject_model_instruction")
.and_then(Value::as_bool)
{
self.inject_model_instruction = Some(inject_model_instruction);
}
}
fn effective_enabled(self) -> bool {
self.enabled.unwrap_or(false)
}
fn effective_inject_model_instruction(self) -> bool {
self.inject_model_instruction.unwrap_or(true)
}
}
pub(crate) fn request_identity_response_encoding_when_redacted(
@@ -122,7 +111,7 @@ pub(crate) async fn resolve_provider_chat_pii_redaction<'a>(
&body_bytes,
format,
build_redaction_session_config(hmac_key, &runtime_config, now_unix_secs),
MaskChatRequestOptions::runtime(feature_settings.effective_inject_model_instruction()),
MaskChatRequestOptions::runtime(),
Some(&cache),
)
.await
@@ -190,3 +179,22 @@ fn redaction_mask_error_to_gateway_error(error: RedactionMaskError) -> GatewayEr
},
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::ChatPiiRedactionFeatureSettings;
#[test]
fn chat_pii_redaction_feature_settings_only_control_enablement() {
let mut settings = ChatPiiRedactionFeatureSettings::default();
settings.merge_from_value(Some(&json!({
"chat_pii_redaction": {
"enabled": true
}
})));
assert!(settings.effective_enabled());
}
}
@@ -3,6 +3,7 @@ use std::collections::BTreeMap;
use super::{
apply_codex_openai_responses_special_body_edits, apply_codex_openai_responses_special_headers,
};
use crate::ai_serving::planner::standard::build_local_openai_responses_request_body;
use http::{HeaderMap, HeaderValue};
use serde_json::json;
@@ -36,6 +37,40 @@ fn applies_codex_defaults_when_body_rules_do_not_handle_fields() {
assert!(body.get("reasoning").is_none());
}
#[test]
fn local_openai_responses_codex_body_wraps_string_input_for_backend() {
let body = json!({
"model": "gpt-5",
"input": "hello"
});
let provider_request_body = build_local_openai_responses_request_body(
&body,
"gpt-5-upstream",
false,
false,
"codex",
"openai:responses",
None,
Some("key-123"),
&HeaderMap::new(),
false,
)
.expect("codex local openai responses body should build");
assert_eq!(
provider_request_body["input"],
json!([{
"type": "message",
"role": "user",
"content": [{
"type": "input_text",
"text": "hello"
}]
}])
);
}
#[test]
fn strips_store_for_compact_even_when_body_rules_handle_it() {
let body_rules = json!([
@@ -352,7 +352,8 @@ fn normalize_chat_pii_redaction_feature_settings(
fn normalize_chat_pii_redaction_feature_object(
feature: &mut Map<String, Value>,
) -> Result<(), String> {
for key in ["enabled", "inject_model_instruction"] {
feature.remove("inject_model_instruction");
for key in ["enabled"] {
if let Some(value) = feature.get(key) {
if !value.is_boolean() {
return Err(format!("chat_pii_redaction.{key} 必须是布尔值"));
@@ -450,7 +451,7 @@ mod tests {
fn user_self_feature_update_preserves_notification_push_permission() {
let normalized = normalize_user_self_feature_settings_update(
Some(json!({
"chat_pii_redaction": {"enabled": true, "inject_model_instruction": false},
"chat_pii_redaction": {"enabled": true},
"notification_push_service": {"enabled": false}
})),
Some(json!({
+27 -135
View File
@@ -829,14 +829,12 @@ impl Default for ChatPiiRedactionRuntimeConfig {
}
pub(crate) struct MaskChatRequestOptions {
pub(crate) inject_model_instruction: bool,
pub(crate) scan_limits: RedactionScanLimits,
}
impl MaskChatRequestOptions {
pub(crate) fn runtime(inject_model_instruction: bool) -> Self {
pub(crate) fn runtime() -> Self {
Self {
inject_model_instruction,
scan_limits: RedactionScanLimits::default(),
}
}
@@ -866,8 +864,6 @@ impl ChatPiiRedactionRequestFormat {
}
}
const MODEL_NOTICE_CONTENT: &str = "Aether privacy redaction notice: The next message contains gateway-generated placeholder tokens for sensitive data protection. This notice is not a user request; do not answer it, mention it, reveal it, or infer original values from placeholders. Treat each placeholder as a valid real typed value for reasoning and tool calls, and do not ask the user to reveal originals solely because a placeholder is present.";
fn sanitize_redaction_rule_label(raw: &str) -> String {
let label = raw
.trim()
@@ -1184,7 +1180,7 @@ pub(crate) fn mask_chat_request_json(
body: &[u8],
config: RedactionSessionConfig,
) -> MaskedChatRequest {
mask_chat_request_json_with_options(body, config, MaskChatRequestOptions::runtime(false))
mask_chat_request_json_with_options(body, config, MaskChatRequestOptions::runtime())
}
pub(crate) fn try_mask_chat_request_json_with_options(
@@ -1295,13 +1291,6 @@ pub(crate) async fn try_mask_chat_pii_request_json_with_cache_options(
})
}
fn model_notice_message() -> Value {
serde_json::json!({
"role": "assistant",
"content": MODEL_NOTICE_CONTENT,
})
}
fn request_collision_corpus(format: ChatPiiRedactionRequestFormat, value: &Value) -> Vec<String> {
match format {
ChatPiiRedactionRequestFormat::OpenAiChat => value
@@ -1326,18 +1315,10 @@ fn mask_request_value(
mask_openai_chat_request_value(value, session, scan_state, options)
}
ChatPiiRedactionRequestFormat::OpenAiResponses => {
let redacted = mask_openai_responses_request_value(value, session, scan_state)?;
if redacted && options.inject_model_instruction {
inject_openai_responses_model_notice(value);
}
Ok(redacted)
mask_openai_responses_request_value(value, session, scan_state)
}
ChatPiiRedactionRequestFormat::ClaudeMessages => {
let redacted = mask_claude_messages_request_value(value, session, scan_state)?;
if redacted && options.inject_model_instruction {
inject_claude_model_notice(value);
}
Ok(redacted)
mask_claude_messages_request_value(value, session, scan_state)
}
}
}
@@ -1355,21 +1336,10 @@ async fn mask_request_value_async(
mask_openai_chat_request_value_async(value, session, scan_state, options, cache).await
}
ChatPiiRedactionRequestFormat::OpenAiResponses => {
let redacted =
mask_openai_responses_request_value_async(value, session, scan_state, cache)
.await?;
if redacted && options.inject_model_instruction {
inject_openai_responses_model_notice(value);
}
Ok(redacted)
mask_openai_responses_request_value_async(value, session, scan_state, cache).await
}
ChatPiiRedactionRequestFormat::ClaudeMessages => {
let redacted =
mask_claude_messages_request_value_async(value, session, scan_state, cache).await?;
if redacted && options.inject_model_instruction {
inject_claude_model_notice(value);
}
Ok(redacted)
mask_claude_messages_request_value_async(value, session, scan_state, cache).await
}
}
}
@@ -1385,16 +1355,10 @@ fn mask_openai_chat_request_value(
};
let mut redacted = false;
let mut notice_inserted = false;
let mut index = 0;
while index < messages.len() {
let message_redacted = mask_chat_message_value(&mut messages[index], session, scan_state)?;
redacted |= message_redacted;
if options.inject_model_instruction && message_redacted && !notice_inserted {
messages.insert(index, model_notice_message());
notice_inserted = true;
index += 1;
}
index += 1;
}
Ok(redacted)
@@ -1412,17 +1376,11 @@ async fn mask_openai_chat_request_value_async(
};
let mut redacted = false;
let mut notice_inserted = false;
let mut index = 0;
while index < messages.len() {
let message_redacted =
mask_chat_message_value_async(&mut messages[index], session, scan_state, cache).await?;
redacted |= message_redacted;
if options.inject_model_instruction && message_redacted && !notice_inserted {
messages.insert(index, model_notice_message());
notice_inserted = true;
index += 1;
}
index += 1;
}
Ok(redacted)
@@ -2150,56 +2108,6 @@ async fn mask_json_string_async(
Ok(true)
}
fn inject_openai_responses_model_notice(value: &mut Value) {
let Some(request) = value.as_object_mut() else {
return;
};
match request.get_mut("instructions") {
Some(Value::String(instructions)) => prepend_model_notice(instructions),
Some(_) => {}
None => {
request.insert(
"instructions".to_string(),
Value::String(MODEL_NOTICE_CONTENT.to_string()),
);
}
}
}
fn inject_claude_model_notice(value: &mut Value) {
let Some(request) = value.as_object_mut() else {
return;
};
match request.get_mut("system") {
Some(Value::String(system)) => prepend_model_notice(system),
Some(Value::Array(parts)) => parts.insert(
0,
serde_json::json!({
"type": "text",
"text": MODEL_NOTICE_CONTENT,
}),
),
Some(_) => {}
None => {
request.insert(
"system".to_string(),
Value::String(MODEL_NOTICE_CONTENT.to_string()),
);
}
}
}
fn prepend_model_notice(text: &mut String) {
if text.contains(MODEL_NOTICE_CONTENT) {
return;
}
if text.trim().is_empty() {
*text = MODEL_NOTICE_CONTENT.to_string();
} else {
*text = format!("{MODEL_NOTICE_CONTENT}\n\n{text}");
}
}
pub(crate) struct RestoredSyncResponseBody {
pub(crate) body: Vec<u8>,
pub(crate) restored: bool,
@@ -4408,7 +4316,7 @@ mod tests {
&raw,
ChatPiiRedactionRequestFormat::ClaudeMessages,
test_config(),
MaskChatRequestOptions::runtime(true),
MaskChatRequestOptions::runtime(),
)
.expect("claude messages request should mask");
@@ -4417,11 +4325,7 @@ mod tests {
let masked_json: serde_json::Value =
serde_json::from_slice(&masked.body).expect("masked request should stay valid JSON");
assert_eq!(masked_json["metadata"]["owner"], "[email protected]");
assert!(masked_json["system"][0]["text"]
.as_str()
.expect("notice should remain a string")
.contains("Aether privacy redaction notice"));
assert!(!masked_json["system"][1]["text"]
assert!(!masked_json["system"][0]["text"]
.as_str()
.expect("system text should remain a string")
.contains("[email protected]"));
@@ -4454,7 +4358,7 @@ mod tests {
&raw,
ChatPiiRedactionRequestFormat::OpenAiChat,
test_config(),
MaskChatRequestOptions::runtime(false),
MaskChatRequestOptions::runtime(),
)
.expect("chat request should mask");
@@ -4497,7 +4401,7 @@ mod tests {
&raw,
ChatPiiRedactionRequestFormat::OpenAiResponses,
test_config(),
MaskChatRequestOptions::runtime(true),
MaskChatRequestOptions::runtime(),
)
.expect("responses request should mask");
@@ -4509,7 +4413,6 @@ mod tests {
let instructions = masked_json["instructions"]
.as_str()
.expect("instructions should remain a string");
assert!(instructions.contains("Aether privacy redaction notice"));
assert!(!instructions.contains("[email protected]"));
assert!(!masked_json["input"][0]["content"][0]["text"]
.as_str()
@@ -4995,7 +4898,7 @@ mod tests {
let masked = mask_chat_request_json_with_options(
&serde_json::to_vec(&request).expect("request should serialize"),
build_redaction_session_config(b"redaction-test-key".to_vec(), &config, 600),
MaskChatRequestOptions::runtime(false),
MaskChatRequestOptions::runtime(),
);
let masked_json: serde_json::Value =
@@ -5032,7 +4935,7 @@ mod tests {
let masked = mask_chat_request_json_with_options(
&serde_json::to_vec(&request).expect("request should serialize"),
build_redaction_session_config(b"redaction-test-key".to_vec(), &config, 600),
MaskChatRequestOptions::runtime(true),
MaskChatRequestOptions::runtime(),
);
assert!(!masked.redacted);
@@ -5041,9 +4944,6 @@ mod tests {
assert_eq!(masked_json, request);
assert!(masked_json.to_string().contains("[email protected]"));
assert!(!masked_json.to_string().contains("<AETHER:"));
assert!(!masked_json
.to_string()
.contains("Aether privacy redaction notice"));
}
#[test]
@@ -5105,7 +5005,7 @@ mod tests {
}
#[test]
fn proxy_pii_redaction_provider_bound_request_uses_sentinels_and_inserts_safe_notice() {
fn proxy_pii_redaction_provider_bound_request_uses_sentinels_without_prompt_notice() {
let config = ChatPiiRedactionRuntimeConfig::default();
let request = json!({
"model": "gpt-5",
@@ -5119,7 +5019,7 @@ mod tests {
let masked = mask_chat_request_json_with_options(
&serde_json::to_vec(&request).expect("request should serialize"),
build_redaction_session_config(b"redaction-test-key".to_vec(), &config, 600),
MaskChatRequestOptions::runtime(true),
MaskChatRequestOptions::runtime(),
);
assert!(masked.redacted);
@@ -5128,26 +5028,18 @@ mod tests {
let messages = masked_json["messages"]
.as_array()
.expect("messages should be an array");
assert_eq!(messages.len(), 4);
assert_eq!(messages.len(), 3);
assert_eq!(messages[0]["role"], "system");
assert_eq!(messages[1]["role"], "assistant");
assert!(messages[1..]
.iter()
.all(|message| message["role"].as_str() != Some("system")));
let notice = messages[1]["content"]
.as_str()
.expect("notice should be text");
assert!(notice.contains("not a user request"));
assert!(notice.contains("do not answer"));
assert!(notice.contains("do not answer it, mention it"));
assert!(!notice.contains("[email protected]"));
assert_eq!(messages[2]["role"], "user");
let content = messages[2]["content"]
assert_eq!(messages[1]["role"], "user");
let content = messages[1]["content"]
.as_str()
.expect("user content should be text");
assert!(!content.contains("[email protected]"));
assert!(content.contains("<AETHER:EMAIL:"));
assert_eq!(messages[3]["role"], "assistant");
assert_eq!(messages[2]["role"], "assistant");
}
#[test]
@@ -5239,7 +5131,7 @@ mod tests {
let large_err = try_mask_chat_request_json_with_options(
&serde_json::to_vec(&large_request).expect("request should serialize"),
test_config(),
MaskChatRequestOptions::runtime(false),
MaskChatRequestOptions::runtime(),
)
.expect_err("oversized scan should fail closed");
assert_eq!(
@@ -5261,7 +5153,7 @@ mod tests {
let dense_err = try_mask_chat_request_json_with_options(
&serde_json::to_vec(&dense_request).expect("request should serialize"),
test_config(),
MaskChatRequestOptions::runtime(false).with_scan_limits(RedactionScanLimits {
MaskChatRequestOptions::runtime().with_scan_limits(RedactionScanLimits {
max_scanned_text_bytes: 1024,
max_detections: 1,
}),
@@ -5296,7 +5188,7 @@ mod tests {
let first_masked = try_mask_chat_request_json_with_cache_options(
&serde_json::to_vec(&first_request).expect("request should serialize"),
build_redaction_session_config(b"redaction-test-key".to_vec(), &config, 600),
MaskChatRequestOptions::runtime(false),
MaskChatRequestOptions::runtime(),
Some(&cache),
)
.await
@@ -5315,7 +5207,7 @@ mod tests {
let second_masked = try_mask_chat_request_json_with_cache_options(
&serde_json::to_vec(&second_request).expect("request should serialize"),
build_redaction_session_config(b"redaction-test-key".to_vec(), &config, 899),
MaskChatRequestOptions::runtime(false),
MaskChatRequestOptions::runtime(),
Some(&cache),
)
.await
@@ -5356,7 +5248,7 @@ mod tests {
let rolled_masked = try_mask_chat_request_json_with_cache_options(
&serde_json::to_vec(&second_request).expect("request should serialize"),
build_redaction_session_config(b"redaction-test-key".to_vec(), &config, 900),
MaskChatRequestOptions::runtime(false),
MaskChatRequestOptions::runtime(),
Some(&cache),
)
.await
@@ -5420,7 +5312,7 @@ mod tests {
let first_masked = try_mask_chat_request_json_with_cache_options(
&serde_json::to_vec(&first_request).expect("request should serialize"),
build_redaction_session_config(b"redaction-test-key".to_vec(), &config, 600),
MaskChatRequestOptions::runtime(false),
MaskChatRequestOptions::runtime(),
Some(&cache),
)
.await
@@ -5454,7 +5346,7 @@ mod tests {
let second_masked = try_mask_chat_request_json_with_cache_options(
&serde_json::to_vec(&colliding_request).expect("request should serialize"),
build_redaction_session_config(b"redaction-test-key".to_vec(), &config, 899),
MaskChatRequestOptions::runtime(false),
MaskChatRequestOptions::runtime(),
Some(&cache),
)
.await
@@ -5525,7 +5417,7 @@ mod tests {
let masked = try_mask_chat_request_json_with_cache_options(
&serde_json::to_vec(&request).expect("request should serialize"),
build_redaction_session_config(b"redaction-test-key".to_vec(), &config, 600),
MaskChatRequestOptions::runtime(false),
MaskChatRequestOptions::runtime(),
Some(&cache),
)
.await
@@ -5570,7 +5462,7 @@ mod tests {
let masked = try_mask_chat_request_json_with_cache_options(
&serde_json::to_vec(&request).expect("request should serialize"),
build_redaction_session_config(b"redaction-test-key".to_vec(), &config, 600),
MaskChatRequestOptions::runtime(false),
MaskChatRequestOptions::runtime(),
Some(&cache),
)
.await
@@ -112,7 +112,6 @@ fn auth_repository_with_redaction_feature_settings() -> Arc<InMemoryAuthApiKeySn
Some(json!({
"chat_pii_redaction": {
"enabled": true,
"inject_model_instruction": true,
}
})),
)]),
@@ -112,7 +112,6 @@ async fn proxy_pii_redaction_local_openai_chat_runtime_masks_headers_and_restore
Some(json!({
"chat_pii_redaction": {
"enabled": true,
"inject_model_instruction": true,
}
})),
)]),
@@ -363,13 +362,7 @@ async fn proxy_pii_redaction_local_openai_chat_runtime_masks_headers_and_restore
let provider_body_text = serde_json::to_string(&seen.body).expect("body should serialize");
assert!(!provider_body_text.contains("[email protected]"));
assert!(provider_body_text.contains("<AETHER:EMAIL:"));
assert_eq!(seen.body["messages"][0]["role"], "assistant");
let notice = seen.body["messages"][0]["content"]
.as_str()
.expect("notice should be text");
assert!(notice.contains("not a user request"));
assert!(notice.contains("do not answer"));
assert_eq!(seen.body["messages"][1]["role"], "user");
assert_eq!(seen.body["messages"][0]["role"], "user");
let stored_candidates = request_candidate_repository
.list_by_request_id("trace-proxy-pii-redaction-sync")
@@ -230,14 +230,10 @@ fn redaction_test_rules() -> serde_json::Value {
])
}
fn chat_pii_redaction_feature_settings(
enabled: bool,
inject_model_instruction: bool,
) -> serde_json::Value {
fn chat_pii_redaction_feature_settings(enabled: bool) -> serde_json::Value {
json!({
"chat_pii_redaction": {
"enabled": enabled,
"inject_model_instruction": inject_model_instruction,
}
})
}
@@ -245,7 +241,6 @@ fn chat_pii_redaction_feature_settings(
fn auth_repository_with_redaction_feature_settings(
test_id: &str,
feature_enabled: bool,
inject_model_instruction: bool,
) -> Arc<InMemoryAuthApiKeySnapshotRepository> {
let snapshot = auth_snapshot(&format!("api-key-{test_id}"), &format!("user-{test_id}"));
let key_hash = hash_api_key(&format!("sk-client-{test_id}"));
@@ -257,10 +252,7 @@ fn auth_repository_with_redaction_feature_settings(
.with_export_records(vec![auth_export_record(
&snapshot,
key_hash,
Some(chat_pii_redaction_feature_settings(
feature_enabled,
inject_model_instruction,
)),
Some(chat_pii_redaction_feature_settings(feature_enabled)),
)]),
)
}
@@ -372,8 +364,7 @@ async fn run_sync_redaction_case_with_system_config(
}),
);
let (provider_url, provider_handle) = start_server(provider_app).await;
let auth_repository =
auth_repository_with_redaction_feature_settings(test_id, feature_enabled, true);
let auth_repository = auth_repository_with_redaction_feature_settings(test_id, feature_enabled);
let candidate_selection_repository =
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
candidate_row(test_id),
@@ -522,14 +513,9 @@ async fn ai_execute_sync_pii_redaction_round_trip_impl() {
assert!(provider_body_text.contains("<AETHER:ACCESS_TOKEN:"));
assert!(provider_body_text.contains("<AETHER:SECRET_KEY:"));
assert_eq!(seen.body["messages"][0]["role"], "system");
assert_eq!(seen.body["messages"][1]["role"], "assistant");
let notice = seen.body["messages"][1]["content"]
.as_str()
.expect("notice should be text");
assert!(notice.contains("not a user request"));
assert_eq!(seen.body["messages"][2]["role"], "user");
assert_eq!(seen.body["messages"][3]["role"], "assistant");
assert_eq!(seen.body["messages"][4]["role"], "tool");
assert_eq!(seen.body["messages"][1]["role"], "user");
assert_eq!(seen.body["messages"][2]["role"], "assistant");
assert_eq!(seen.body["messages"][3]["role"], "tool");
let response_content = response_json["choices"][0]["message"]["content"]
.as_str()
@@ -702,7 +688,7 @@ async fn ai_execute_pii_redaction_restores_executed_candidate_session_after_late
);
let (provider_url, provider_handle) = start_server(provider_app).await;
let auth_repository =
auth_repository_with_redaction_feature_settings("redaction-candidate-session", true, true);
auth_repository_with_redaction_feature_settings("redaction-candidate-session", true);
let mut later_candidate = candidate_row("redaction-candidate-session");
later_candidate.provider_id = "provider-redaction-candidate-session-later".to_string();
later_candidate.endpoint_id = "endpoint-redaction-candidate-session-later".to_string();
@@ -817,7 +803,7 @@ async fn pii_redaction_performance_limits_do_not_forward_unredacted_body_upstrea
);
let (provider_url, provider_handle) = start_server(provider_app).await;
let auth_repository =
auth_repository_with_redaction_feature_settings("pii-redaction-limit", true, true);
auth_repository_with_redaction_feature_settings("pii-redaction-limit", true);
let candidate_selection_repository =
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
candidate_row("pii-redaction-limit"),
@@ -893,7 +879,7 @@ async fn ai_execute_pii_redaction_missing_encryption_key_fails_closed_before_pro
);
let (execution_runtime_url, execution_runtime_handle) = start_server(execution_runtime).await;
let test_id = "ai-execute-pii-redaction-missing-encryption-key";
let auth_repository = auth_repository_with_redaction_feature_settings(test_id, true, true);
let auth_repository = auth_repository_with_redaction_feature_settings(test_id, true);
let candidate_selection_repository =
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
candidate_row(test_id),
@@ -498,8 +498,7 @@ fn auth_repository(case: &RedactionFormatCase) -> Arc<InMemoryAuthApiKeySnapshot
key_hash,
Some(json!({
"chat_pii_redaction": {
"enabled": true,
"inject_model_instruction": true
"enabled": true
}
})),
)]),
@@ -4999,8 +4999,7 @@ async fn gateway_updates_users_me_detail_locally_without_proxying_upstream() {
"username": "alice-updated",
"feature_settings": {
"chat_pii_redaction": {
"enabled": true,
"inject_model_instruction": false
"enabled": true
}
}
}))
@@ -5034,10 +5033,6 @@ async fn gateway_updates_users_me_detail_locally_without_proxying_upstream() {
get_payload["feature_settings"]["chat_pii_redaction"]["enabled"],
true
);
assert_eq!(
get_payload["feature_settings"]["chat_pii_redaction"]["inject_model_instruction"],
false
);
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
gateway_handle.abort();
@@ -7167,8 +7162,7 @@ async fn gateway_handles_users_me_api_key_writes_locally_without_proxying_upstre
"concurrent_limit": 4,
"feature_settings": {
"chat_pii_redaction": {
"enabled": true,
"inject_model_instruction": false
"enabled": true
}
}
}))
@@ -7187,10 +7181,6 @@ async fn gateway_handles_users_me_api_key_writes_locally_without_proxying_upstre
update_payload["feature_settings"]["chat_pii_redaction"]["enabled"],
true
);
assert_eq!(
update_payload["feature_settings"]["chat_pii_redaction"]["inject_model_instruction"],
false
);
assert_eq!(update_payload["message"], "API密钥已更新");
let toggle_response = client