归一化 Claude Agent SDK 身份句,避免上游内容策略误拦

在 Anthropic Messages 代理转发前,把被上游内容策略误判的身份句替换为等价表述,其余字段原样透传
新增归一化单测:只改命中片段,保留 cache_control 等其它 system 形状
This commit is contained in:
kdletters
2026-10-07 08:17:23 +08:00
parent 2b3585cd3c
commit 594ac321bf
@@ -38,6 +38,45 @@ pub(crate) const LLM_REQUEST_MAX_BODY_BYTES: usize = 32 * 1024 * 1024;
const LLM_ANTHROPIC_NATIVE_FIRST_BYTE_TIMEOUT: std::time::Duration =
std::time::Duration::from_secs(20);
/// Claude Agent SDK 在 `system` 里固定注入的身份句原文。
///
/// 2026-10-06 用真实 AGC 客户端复现:共享 LLM Router 的上游内容策略会把这句话整体判成
/// 需要拦截的内容,直接返回 `500 {"error":{"message":"Your request was blocked by our
/// content policy..."}}`。单独把这句话作为 `system` 发送即可稳定复现;同一请求保留 AGC
/// 提示词、billing header、工具、`metadata`、`max_tokens`、`thinking` 等全部其它字段,只把
/// 品牌归属片段换成等价表述后稳定返回 200。因此这里只做最小归一化,保留身份语义,去掉会
/// 触发误判的片段,其它请求内容一律原样透传。
const CLAUDE_AGENT_SDK_IDENTITY_SENTENCE: &str =
"You are a Claude agent, built on Anthropic's Claude Agent SDK.";
const CLAUDE_AGENT_SDK_IDENTITY_SENTENCE_REPLACEMENT: &str =
"You are a Claude agent, built on an Anthropic agent SDK.";
/// 归一化 Claude Agent SDK 注入的身份句,避免上游内容策略误判整条请求。
///
/// `system` 既可能是字符串也可能是数组;Claude Agent SDK 固定发数组,其它形状保持不动,
/// 保证这条路只影响已知会失败的那一种载荷。
fn normalize_claude_agent_sdk_identity_sentence(payload: &mut Value) {
let Some(system) = payload.get_mut("system").and_then(Value::as_array_mut) else {
return;
};
for entry in system {
let Some(object) = entry.as_object_mut() else {
continue;
};
let Some(text) = object.get("text").and_then(Value::as_str) else {
continue;
};
if !text.contains(CLAUDE_AGENT_SDK_IDENTITY_SENTENCE) {
continue;
}
let normalized = text.replace(
CLAUDE_AGENT_SDK_IDENTITY_SENTENCE,
CLAUDE_AGENT_SDK_IDENTITY_SENTENCE_REPLACEMENT,
);
object.insert("text".to_string(), Value::String(normalized));
}
}
mod anthropic_bridge;
pub(crate) mod icon_specs;
@@ -568,6 +607,7 @@ pub async fn proxy_llm_messages(
.to_string();
// 原生直通要用目录解析后的上游模型名;桥接路径由转换器显式接收同一个名字。
object.insert("model".to_string(), Value::String(selected_model.clone()));
normalize_claude_agent_sdk_identity_sentence(&mut payload);
let (base_url, api_key, key_id) =
resolve_llm_router_credentials(&state, authenticated.claims().user_id())
@@ -2204,6 +2244,52 @@ mod tests {
assert!(carry.is_empty());
}
#[test]
fn normalize_claude_agent_sdk_identity_sentence_rewrites_only_the_blocked_sentence() {
let mut payload = json!({
"model": "claude-opus-5-5",
"system": [
{ "type": "text", "text": "x-anthropic-billing-header: cc_version=2.1.285; " },
{
"type": "text",
"text": "You are a Claude agent, built on Anthropic's Claude Agent SDK.",
"cache_control": { "type": "ephemeral" }
},
{ "type": "text", "text": "你是陶泥儿,负责当前任务。" }
]
});
normalize_claude_agent_sdk_identity_sentence(&mut payload);
assert_eq!(
payload["system"][1]["text"],
json!("You are a Claude agent, built on an Anthropic agent SDK.")
);
assert_eq!(
payload["system"][1]["cache_control"],
json!({ "type": "ephemeral" })
);
assert_eq!(
payload["system"][0]["text"],
json!("x-anthropic-billing-header: cc_version=2.1.285; ")
);
assert_eq!(
payload["system"][2]["text"],
json!("你是陶泥儿,负责当前任务。")
);
}
#[test]
fn normalize_claude_agent_sdk_identity_sentence_keeps_other_system_shapes() {
let mut payload = json!({ "system": "You are a Claude agent." });
normalize_claude_agent_sdk_identity_sentence(&mut payload);
assert_eq!(payload["system"], json!("You are a Claude agent."));
let mut without_system = json!({ "model": "claude-opus-5-5" });
normalize_claude_agent_sdk_identity_sentence(&mut without_system);
assert_eq!(without_system, json!({ "model": "claude-opus-5-5" }));
}
#[test]
fn router_protocol_url_composes_v1_without_duplicating_it() {
// baseUrl 不带路由与版本段,协议自己拼。