Files
Aether/src/core/api_format/headers.py
fawney19 fdb50a065b refactor(fingerprint): 移除请求头构建路径中的 per-key 指纹定制化注入
headers.py 删除 build_browser_fingerprint_headers / build_anthropic_extra_headers
函数及相关辅助,改为直接使用固定常量;PassthroughRequestBuilder 移除
Claude 格式的指纹注入步骤;Antigravity constants 移除 per-key 指纹
覆盖逻辑,统一使用进程级固定值。
2026-03-05 10:24:52 +08:00

674 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
统一的请求头处理模块
职责:
1. 请求头规范化(大小写统一)
2. 客户端 API Key 提取
3. 能力需求检测
4. 上游请求头构建
5. 响应头过滤
6. 日志脱敏
"""
from __future__ import annotations
import json
from collections.abc import Set as AbstractSet
from typing import Any
from src.core.api_format.enums import ApiFamily
from src.core.api_format.metadata import (
get_auth_config_for_endpoint,
get_extra_headers_for_endpoint,
get_protected_keys_for_endpoint,
resolve_endpoint_definition,
)
from src.core.api_format.signature import EndpointSignature, parse_signature_key
from src.core.logger import logger
# =============================================================================
# 头部常量定义
# =============================================================================
# 通用浏览器指纹 Headers用于绕过 Cloudflare 等反爬防护
# 基于 Electron 桌面客户端的真实请求头构建,作为所有 adapter 请求的底层默认值
BROWSER_FINGERPRINT_HEADERS: dict[str, str] = {
"User-Agent": (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/140.0.7339.249 Electron/38.7.0 Safari/537.36"
),
"Accept": "application/json",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN",
"sec-ch-ua": '"Not=A?Brand";v="24", "Chromium";v="140"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"macOS"',
"Sec-Fetch-Site": "cross-site",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Dest": "empty",
}
# Anthropic/Claude 专属 Headers仅 Claude API family 使用)
# 包含 Stainless SDK 指纹和 direct-browser-access 标记
_ANTHROPIC_EXTRA_HEADERS: dict[str, str] = {
"anthropic-dangerous-direct-browser-access": "true",
"x-stainless-os": "Unknown",
"x-stainless-runtime": "browser:chrome",
"x-stainless-arch": "unknown",
"x-stainless-lang": "js",
"x-stainless-package-version": "0.41.0",
"x-stainless-runtime-version": "140.0.7339",
"x-stainless-retry-count": "0",
}
# 转发给上游时需要剔除的头部(系统管理 + 认证替换 + 客户端/代理元数据)
UPSTREAM_DROP_HEADERS: frozenset[str] = frozenset(
{
# 认证头 - 会被替换为 Provider 的认证
"authorization",
"x-api-key",
"x-goog-api-key",
# 系统管理头 - 由 HTTP 客户端重新生成
"host",
"content-length",
"transfer-encoding",
"connection",
# 编码头 - 丢弃客户端值,由 BROWSER_FINGERPRINT_HEADERS 统一设置
"accept-encoding",
"content-encoding",
# 反向代理 / 网关注入的头部 - 属于本站基础设施,不应泄露给上游
"x-real-ip",
"x-real-proto",
"x-forwarded-for",
"x-forwarded-proto",
"x-forwarded-host",
"x-forwarded-port",
}
)
# 最小必脱敏集合(编译时常量,用于快速路径)
# 完整脱敏应使用 SystemConfigService.get_sensitive_headers()
CORE_REDACT_HEADERS: frozenset[str] = frozenset(
{
"authorization",
"x-api-key",
"x-goog-api-key",
}
)
# Hop-by-hop 头部 (RFC 7230)
HOP_BY_HOP_HEADERS: frozenset[str] = frozenset(
{
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailer",
"transfer-encoding",
"upgrade",
}
)
# 响应时需要过滤的头部body-dependent + hop-by-hop
RESPONSE_DROP_HEADERS: frozenset[str] = (
frozenset(
{
"content-length",
"content-encoding",
"transfer-encoding",
"content-type",
}
)
| HOP_BY_HOP_HEADERS
)
# =============================================================================
# 请求头规范化
# =============================================================================
def normalize_headers(headers: dict[str, str]) -> dict[str, str]:
"""
将请求头 key 统一为小写
用于处理 context.original_headers 的大小写敏感问题。
"""
return {k.lower(): v for k, v in headers.items()}
def get_header_value(headers: dict[str, str], key: str, default: str = "") -> str:
"""
大小写不敏感地获取请求头值
Args:
headers: 原始请求头(可能大小写不一致)
key: 要获取的 key任意大小写
default: 未找到时的默认值
Returns:
头部值,未找到返回 default
"""
key_lower = key.lower()
for k, v in headers.items():
if k.lower() == key_lower:
return v
return default
# =============================================================================
# 客户端 API Key 提取
# =============================================================================
def extract_client_api_key_for_endpoint(
headers: dict[str, str],
endpoint: str | EndpointSignature | tuple,
) -> str | None:
"""
新模式:从客户端请求头提取 API Key。
Args:
headers: 原始请求头(自动处理大小写)
endpoint: endpoint signature`family:kind` / EndpointSignature / (ApiFamily, EndpointKind)
"""
auth_header, auth_type = get_auth_config_for_endpoint(endpoint)
value = get_header_value(headers, auth_header)
if not value:
return None
if auth_type == "bearer":
if value.lower().startswith("bearer "):
return value[7:]
return None
return value
def extract_client_api_key_for_endpoint_with_query(
headers: dict[str, str],
query_params: dict[str, str] | None,
endpoint: str | EndpointSignature | tuple,
) -> str | None:
"""
新模式:从客户端请求头或 URL 参数提取 API Key。
Gemini family 优先级:
1. URL 参数 ?key=
2. x-goog-api-key 请求头
"""
try:
sig = (
endpoint
if isinstance(endpoint, EndpointSignature)
else (
parse_signature_key(endpoint) # type: ignore[arg-type]
if isinstance(endpoint, str)
else EndpointSignature(api_family=endpoint[0], endpoint_kind=endpoint[1])
) # type: ignore[index]
)
except Exception:
sig = None
if sig and sig.api_family.value == "gemini":
query_key = query_params.get("key") if query_params else None
if query_key:
return query_key
return extract_client_api_key_for_endpoint(headers, endpoint)
# =============================================================================
# 能力需求检测
# =============================================================================
def detect_capabilities_for_endpoint(
headers: dict[str, str],
endpoint: str | EndpointSignature | tuple,
request_body: dict[str, Any] | None = None, # noqa: ARG001 - 预留
) -> dict[str, bool]:
"""
新模式:从请求头检测能力需求。
当前支持:
- Claude family: anthropic-beta 头中的 context-1m
"""
requirements: dict[str, bool] = {}
try:
sig = (
endpoint
if isinstance(endpoint, EndpointSignature)
else (
parse_signature_key(endpoint) # type: ignore[arg-type]
if isinstance(endpoint, str)
else EndpointSignature(api_family=endpoint[0], endpoint_kind=endpoint[1])
) # type: ignore[index]
)
except Exception:
sig = None
if sig and sig.api_family.value == "claude":
beta_header = get_header_value(headers, "anthropic-beta")
if "context-1m" in beta_header.lower():
requirements["context_1m"] = True
return requirements
# =============================================================================
# 上游请求头构建
# =============================================================================
class HeaderBuilder:
"""
请求头构建器
使用 lower-case key 索引确保唯一性和确定的优先级。
优先级(后者覆盖前者):原始头部 < endpoint 头部 < extra 头部 < 认证头
"""
def __init__(self) -> None:
# key: (original_case_key, value)
self._headers: dict[str, tuple[str, str]] = {}
def add(self, key: str, value: str) -> HeaderBuilder:
"""添加单个头部(会覆盖同名头部)"""
self._headers[key.lower()] = (key, value)
return self
def add_many(self, headers: dict[str, str]) -> HeaderBuilder:
"""批量添加头部"""
for k, v in headers.items():
self.add(k, v)
return self
def add_protected(
self, headers: dict[str, str], protected_keys: AbstractSet[str]
) -> HeaderBuilder:
"""
添加头部但保护指定的 key 不被覆盖
用于 endpoint 额外请求头不能覆盖认证头的场景。
"""
protected_lower = {k.lower() for k in protected_keys}
for k, v in headers.items():
if k.lower() not in protected_lower:
self.add(k, v)
return self
def remove(self, keys: frozenset[str]) -> HeaderBuilder:
"""移除指定的头部"""
for k in keys:
self._headers.pop(k.lower(), None)
return self
def rename(self, from_key: str, to_key: str) -> HeaderBuilder:
"""
重命名头部(保留原值)
如果 from_key 不存在,则不做任何操作。
"""
from_lower = from_key.lower()
if from_lower in self._headers:
_, value = self._headers.pop(from_lower)
self._headers[to_key.lower()] = (to_key, value)
return self
def apply_rules(
self,
rules: list[dict[str, Any]],
protected_keys: AbstractSet[str] | None = None,
) -> HeaderBuilder:
"""
应用请求头规则
支持的规则类型:
- set: 设置/覆盖头部 {"action": "set", "key": "X-Custom", "value": "fixed"}
- drop: 删除头部 {"action": "drop", "key": "X-Unwanted"}
- rename: 重命名头部 {"action": "rename", "from": "X-Old", "to": "X-New"}
Args:
rules: 规则列表
protected_keys: 受保护的 key不能被 set/drop/rename 修改)
"""
protected_lower = {k.lower() for k in protected_keys} if protected_keys else set()
for rule in rules:
action = rule.get("action")
if action == "set":
key = rule.get("key", "")
value = rule.get("value", "")
if key and key.lower() not in protected_lower:
self.add(key, value)
elif action == "drop":
key = rule.get("key", "")
if key and key.lower() not in protected_lower:
self._headers.pop(key.lower(), None)
elif action == "rename":
from_key = rule.get("from", "")
to_key = rule.get("to", "")
if from_key and to_key:
# 两个 key 都不能是受保护的
if (
from_key.lower() not in protected_lower
and to_key.lower() not in protected_lower
):
self.rename(from_key, to_key)
return self
def build(self) -> dict[str, str]:
"""构建最终的头部字典"""
result: dict[str, str] = {}
for original_key, value in self._headers.values():
result[original_key] = _normalize_header_value_for_httpx(original_key, value)
return result
def _normalize_header_value_for_httpx(key: str, value: str) -> str:
"""将 header 值归一化为 httpx/h11 可发送的 ASCII 字符串。
说明:
- 当前 httpx/h11 栈会对 str 类型 header 值执行 ASCII 编码。
- 若值包含非 ASCII 字符(如中文),会抛出 UnicodeEncodeError。
- 因此这里统一做 ASCII 归一化,确保请求可稳定发出。
"""
if value.isascii():
return value
key_lower = key.lower()
# Codex CLI 元数据是 JSON 字符串,优先重编码为 ASCII JSON语义最稳定。
if key_lower == "x-codex-turn-metadata":
try:
normalized = json.dumps(json.loads(value), ensure_ascii=True, separators=(",", ":"))
logger.debug(
"Header '{}' contains non-ASCII chars, normalized as ASCII JSON",
key,
)
return normalized
except Exception:
# 非法 JSON 时走通用兜底,避免阻断请求。
pass
# 兜底:仅将非 ASCII 字符替换为 \uXXXX保留 ASCII 字符原样
escaped = "".join(c if c.isascii() else f"\\u{ord(c):04x}" for c in value)
logger.warning(
"Header '{}' contains non-ASCII chars, escaped for httpx compatibility",
key,
)
return escaped
def build_upstream_headers_for_endpoint(
original_headers: dict[str, str],
endpoint: str | EndpointSignature | tuple,
provider_api_key: str,
*,
endpoint_headers: dict[str, str] | None = None,
extra_headers: dict[str, str] | None = None,
drop_headers: frozenset[str] | None = None,
header_rules: list[dict[str, Any]] | None = None,
) -> dict[str, str]:
"""
新模式:构建发送给上游 Provider 的请求头(基于 endpoint signature
优先级(后者覆盖前者):
1. 原始头部(排除 drop_headers
2. endpoint 配置头部
3. header_rules用户自定义的请求头规则支持 set/drop/rename
4. extra_headers
5. 认证头(最高优先级,始终设置)
"""
if drop_headers is None:
drop_headers = UPSTREAM_DROP_HEADERS
auth_header, auth_type = get_auth_config_for_endpoint(endpoint)
auth_value = f"Bearer {provider_api_key}" if auth_type == "bearer" else provider_api_key
protected_keys = {auth_header.lower(), "content-type"}
builder = HeaderBuilder()
for k, v in original_headers.items():
if k.lower() not in drop_headers:
builder.add(k, v)
if endpoint_headers:
builder.add_protected(endpoint_headers, protected_keys)
# 应用用户自定义的请求头规则(认证头受保护)
if header_rules:
builder.apply_rules(header_rules, protected_keys)
if extra_headers:
builder.add_many(extra_headers)
builder.add(auth_header, auth_value)
result = builder.build()
if not any(k.lower() == "content-type" for k in result):
result["Content-Type"] = "application/json"
return result
def merge_headers_with_protection(
base_headers: dict[str, str],
extra_headers: dict[str, str] | None,
protected_keys: frozenset[str] | set[str],
) -> dict[str, str]:
"""
合并头部但保护指定的 key 不被覆盖
等价于原 build_safe_headers 的功能。
Args:
base_headers: 基础头部
extra_headers: 要合并的额外头部
protected_keys: 受保护的 key 集合
Returns:
合并后的头部
"""
if not extra_headers:
return dict(base_headers)
builder = HeaderBuilder()
builder.add_many(base_headers)
builder.add_protected(extra_headers, protected_keys)
return builder.build()
# =============================================================================
# 响应头过滤
# =============================================================================
def filter_response_headers(
headers: dict[str, str] | None,
drop_headers: frozenset[str] | None = None,
) -> dict[str, str]:
"""
过滤上游响应头中不应透传给客户端的字段
Args:
headers: 上游响应头
drop_headers: 要剔除的头部集合None 使用默认值)
Returns:
过滤后的头部
"""
if not headers:
return {}
if drop_headers is None:
drop_headers = RESPONSE_DROP_HEADERS
return {k: v for k, v in headers.items() if k.lower() not in drop_headers}
# =============================================================================
# 日志脱敏
# =============================================================================
def redact_headers_for_log(
headers: dict[str, str],
redact_keys: frozenset[str] | None = None,
) -> dict[str, str]:
"""
将敏感头部值替换为 *** 用于日志记录
Args:
headers: 原始头部
redact_keys: 要脱敏的 key 集合None 使用 CORE_REDACT_HEADERS
Returns:
脱敏后的头部
Note:
完整的脱敏应该使用 SystemConfigService.get_sensitive_headers()
来获取用户配置的敏感头列表。
"""
if redact_keys is None:
redact_keys = CORE_REDACT_HEADERS
return {k: "***" if k.lower() in redact_keys else v for k, v in headers.items()}
# =============================================================================
# Adapter 统一接口
# =============================================================================
def build_adapter_base_headers_for_endpoint(
endpoint: str | EndpointSignature | tuple,
api_key: str,
*,
include_extra: bool = True,
) -> dict[str, str]:
"""
新模式:根据 endpoint signature 构建基础请求头。
浏览器指纹 headers 作为底层默认值注入Claude API family 额外注入 Anthropic 专属 header。
认证头和 extra_headers 会覆盖它们。
"""
auth_header, auth_type = get_auth_config_for_endpoint(endpoint)
auth_value = f"Bearer {api_key}" if auth_type == "bearer" else api_key
# 以浏览器指纹为底层默认值,绕过 Cloudflare 等反爬防护
headers: dict[str, str] = {**BROWSER_FINGERPRINT_HEADERS}
# Claude API family 额外注入 Anthropic 专属 header
definition = resolve_endpoint_definition(endpoint)
if definition and definition.api_family == ApiFamily.CLAUDE:
headers.update(_ANTHROPIC_EXTRA_HEADERS)
headers[auth_header] = auth_value
headers["Content-Type"] = "application/json"
if include_extra:
extra = get_extra_headers_for_endpoint(endpoint)
if extra:
headers.update(extra)
return headers
def build_adapter_headers_for_endpoint(
endpoint: str | EndpointSignature | tuple,
api_key: str,
extra_headers: dict[str, str] | None = None,
) -> dict[str, str]:
"""
新模式:构建完整的 Adapter 请求头(包含 extra_headers
"""
base = build_adapter_base_headers_for_endpoint(endpoint, api_key)
if not extra_headers:
return base
protected = get_protected_keys_for_endpoint(endpoint)
return merge_headers_with_protection(base, extra_headers, protected)
def get_adapter_protected_keys_for_endpoint(
endpoint: str | EndpointSignature | tuple,
) -> tuple[str, ...]:
"""新模式:获取 Adapter 的受保护头部 key。"""
return tuple(get_protected_keys_for_endpoint(endpoint))
# =============================================================================
# Header Rules 工具函数
# =============================================================================
def extract_set_headers_from_rules(
header_rules: list[dict[str, Any]] | None,
) -> dict[str, str] | None:
"""
从 header_rules 中提取 set 操作生成的头部字典
用于需要构造额外请求头的场景(如模型列表查询、模型测试等)。
注意drop 和 rename 操作在这里不适用,因为它们用于修改已存在的头部。
Args:
header_rules: 请求头规则列表 [{"action": "set", "key": "X-Custom", "value": "val"}, ...]
Returns:
set 操作生成的头部字典,如果没有则返回 None
"""
if not header_rules:
return None
headers: dict[str, str] = {}
for rule in header_rules:
if rule.get("action") == "set":
key = rule.get("key", "")
value = rule.get("value", "")
if key:
headers[key] = value
return headers if headers else None
def get_extra_headers_from_endpoint(endpoint: Any) -> dict[str, str] | None:
"""
从 endpoint 提取额外请求头
用于需要构造额外请求头的场景(如模型列表查询、模型测试等)。
Args:
endpoint: ProviderEndpoint 对象
Returns:
额外请求头字典,如果没有则返回 None
"""
header_rules = getattr(endpoint, "header_rules", None)
return extract_set_headers_from_rules(header_rules)
# =============================================================================
# 请求头辅助工具
# =============================================================================
def set_accept_if_absent(headers: dict[str, str], value: str = "text/event-stream") -> None:
"""Set the ``Accept`` header only if not already present (case-insensitive check).
Used by stream handlers to request SSE format from upstream without overriding
provider-specific Accept headers (e.g. Kiro's ``application/vnd.amazon.eventstream``).
"""
if not any(k.lower() == "accept" for k in headers):
headers["Accept"] = value