feat: 新增 frontdoor 执行回环守卫与多项可观测性增强

- 新增 frontdoor_loop_guard 模块,检测并拒绝 execution runtime 回环到本地网关的请求(HTTP 508)
- candidate loop 引入 span tracking、执行尝试日志与流式看门狗超时
- 本地故障转移策略支持从 report_context 加载,新增 append_local_failover_policy_to_value
- runtime tracing 美化:移除 identity 前缀,按 span 深度树形缩进,target 固定宽度展示
- Codex OpenAI CLI 补齐 chatgpt-account-id/x-client-request-id/session_id/conversation_id 请求头
- OpenAI CLI same/cross-format 聚合规则放宽以支持 openai:compact 客户端格式,并过滤 error-like 响应体
- auth/proxy/finalize 日志补充 user_id/api_key_id/api_key_name/balance_remaining 等字段
- 启动日志拆分为 starting/ready/config 三段,新增 resolve_bind_http_base_url
- access_log middleware 将生成的 trace_id 回注到下游请求头
- Cargo.toml 启用 serde_json preserve_order 特性
This commit is contained in:
fawney19
2026-04-11 01:50:24 +08:00
parent 3f057628b7
commit 6144473ebe
38 changed files with 1957 additions and 421 deletions

View File

@@ -211,9 +211,14 @@ pub fn maybe_build_openai_cli_cross_format_sync_product_from_normalized_payload(
.trim()
.to_ascii_lowercase();
if client_api_format != "openai:cli"
|| sync_cli_response_conversion_kind(&provider_api_format, &client_api_format).is_none()
{
if !matches!(client_api_format.as_str(), "openai:cli" | "openai:compact") {
return Ok(None);
}
if !matches!(
provider_api_format.as_str(),
"openai:cli" | "claude:chat" | "claude:cli" | "gemini:chat" | "gemini:cli"
) {
return Ok(None);
}
@@ -434,15 +439,8 @@ fn maybe_build_openai_cli_same_family_sync_body(
.unwrap_or_default()
.trim()
.to_ascii_lowercase();
let needs_conversion = report_context
.get("needs_conversion")
.and_then(Value::as_bool)
.unwrap_or(false);
if !is_openai_cli_family_api_format(&provider_api_format)
|| !is_openai_cli_family_api_format(&client_api_format)
|| provider_api_format != client_api_format
|| needs_conversion
{
return None;
}
@@ -481,15 +479,8 @@ fn maybe_build_openai_cli_same_family_stream_sync_body(
.unwrap_or_default()
.trim()
.to_ascii_lowercase();
let needs_conversion = report_context
.get("needs_conversion")
.and_then(Value::as_bool)
.unwrap_or(false);
if !is_openai_cli_family_api_format(&provider_api_format)
|| !is_openai_cli_family_api_format(&client_api_format)
|| provider_api_format != client_api_format
|| needs_conversion
{
return Ok(None);
}
@@ -521,7 +512,8 @@ fn maybe_build_openai_cross_format_provider_body_from_normalized_payload(
None => None,
};
Ok(aggregated_stream_body.or_else(|| body_json.cloned()))
let provider_body_json = aggregated_stream_body.or_else(|| body_json.cloned());
Ok(provider_body_json.filter(|value| !is_error_like_sync_body(value)))
}
fn is_error_like_sync_body(value: &Value) -> bool {
@@ -1501,27 +1493,28 @@ mod tests {
}
#[test]
fn rejects_openai_cli_same_family_exact_same_stream_when_needs_conversion_is_true() {
fn accepts_openai_cli_same_family_stream_when_needs_conversion_is_true() {
let body = concat!(
"event: response.completed\n",
"data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_123\",\"object\":\"response\",\"model\":\"gpt-5\",\"status\":\"completed\",\"output\":[]}}\n\n",
);
let report_context = json!({
"provider_api_format": "openai:cli",
"client_api_format": "openai:cli",
"client_api_format": "openai:compact",
"needs_conversion": true,
});
let body_json = maybe_build_openai_cli_same_family_sync_body_from_normalized_payload(
"openai_cli_sync_finalize",
"openai_compact_sync_finalize",
200,
Some(&report_context),
None,
Some(&base64::engine::general_purpose::STANDARD.encode(body)),
)
.expect("openai-cli same-family guard should not error");
.expect("openai-cli same-family aggregation should not error")
.expect("aggregated body should exist");
assert!(body_json.is_none());
assert_eq!(body_json["id"], "resp_123");
}
#[test]
@@ -1736,6 +1729,60 @@ mod tests {
);
}
#[test]
fn rejects_openai_cli_cross_format_error_body_json() {
let report_context = json!({
"provider_api_format": "openai:cli",
"client_api_format": "openai:compact",
"model": "gpt-5",
"mapped_model": "gpt-5",
});
let provider_body_json = json!({
"error": {
"message": "quota reached",
"type": "rate_limit_error"
}
});
let product = maybe_build_openai_cli_cross_format_sync_product_from_normalized_payload(
"openai_compact_sync_finalize",
200,
Some(&report_context),
Some(&provider_body_json),
None,
)
.expect("openai-cli cross-format error guard should not error");
assert!(product.is_none());
}
#[test]
fn rejects_openai_cli_cross_format_for_openai_family_provider() {
let report_context = json!({
"provider_api_format": "openai:compact",
"client_api_format": "openai:cli",
"model": "gpt-5",
"mapped_model": "gpt-5",
});
let provider_body_json = json!({
"id": "resp_123",
"object": "response",
"status": "completed",
"output": []
});
let product = maybe_build_openai_cli_cross_format_sync_product_from_normalized_payload(
"openai_cli_sync_finalize",
200,
Some(&report_context),
Some(&provider_body_json),
None,
)
.expect("openai-cli cross-format openai-family guard should not error");
assert!(product.is_none());
}
#[test]
fn rejects_openai_chat_cross_format_for_unsupported_matrix() {
let report_context = json!({

View File

@@ -74,6 +74,17 @@ fn header_map_has_non_empty_value(headers: &http::HeaderMap, header_name: &str)
})
}
fn btree_map_has_non_empty_value(headers: &BTreeMap<String, String>, header_name: &str) -> bool {
let target = header_name.trim().to_ascii_lowercase();
if target.is_empty() {
return false;
}
headers
.iter()
.any(|(name, value)| name.trim().eq_ignore_ascii_case(&target) && !value.trim().is_empty())
}
fn extract_codex_account_id(decrypted_auth_config_raw: Option<&str>) -> Option<String> {
let raw = decrypted_auth_config_raw?.trim();
if raw.is_empty() {
@@ -187,7 +198,42 @@ pub fn apply_codex_openai_cli_special_headers(
.map(str::trim)
.filter(|value| !value.is_empty());
if !header_map_has_non_empty_value(original_headers, "session_id") {
provider_request_headers.insert("session_id".to_string(), prompt_cache_key.unwrap().to_string());
if !header_map_has_non_empty_value(original_headers, "chatgpt-account-id")
&& !btree_map_has_non_empty_value(provider_request_headers, "chatgpt-account-id")
{
if let Some(account_id) = extract_codex_account_id(decrypted_auth_config_raw) {
provider_request_headers.insert("chatgpt-account-id".to_string(), account_id);
}
}
if !header_map_has_non_empty_value(original_headers, "x-client-request-id")
&& !btree_map_has_non_empty_value(provider_request_headers, "x-client-request-id")
{
if let Some(request_id) = request_id.map(str::trim).filter(|value| !value.is_empty()) {
provider_request_headers
.insert("x-client-request-id".to_string(), request_id.to_string());
}
}
let short_session_id = prompt_cache_key.and_then(build_short_codex_header_id);
if !header_map_has_non_empty_value(original_headers, "session_id")
&& !btree_map_has_non_empty_value(provider_request_headers, "session_id")
{
if let Some(short_session_id) = short_session_id.as_deref() {
provider_request_headers.insert("session_id".to_string(), short_session_id.to_string());
}
}
if provider_api_format
.trim()
.eq_ignore_ascii_case("openai:cli")
&& !header_map_has_non_empty_value(original_headers, "conversation_id")
&& !btree_map_has_non_empty_value(provider_request_headers, "conversation_id")
{
if let Some(short_session_id) = short_session_id.as_deref() {
provider_request_headers
.insert("conversation_id".to_string(), short_session_id.to_string());
}
}
}