2026-01-15 22:25:56 +08:00
|
|
|
|
"""
|
|
|
|
|
|
统一的请求头处理模块
|
|
|
|
|
|
|
|
|
|
|
|
职责:
|
|
|
|
|
|
1. 请求头规范化(大小写统一)
|
|
|
|
|
|
2. 客户端 API Key 提取
|
|
|
|
|
|
3. 能力需求检测
|
|
|
|
|
|
4. 上游请求头构建
|
|
|
|
|
|
5. 响应头过滤
|
|
|
|
|
|
6. 日志脱敏
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-02-26 02:56:54 +08:00
|
|
|
|
import json
|
2026-01-30 03:10:21 +08:00
|
|
|
|
from collections.abc import Set as AbstractSet
|
|
|
|
|
|
from typing import Any
|
2026-01-15 22:25:56 +08:00
|
|
|
|
|
2026-02-25 21:59:29 +08:00
|
|
|
|
from src.core.api_format.enums import ApiFamily
|
2026-02-01 17:25:28 +08:00
|
|
|
|
from src.core.api_format.metadata import (
|
|
|
|
|
|
get_auth_config_for_endpoint,
|
|
|
|
|
|
get_extra_headers_for_endpoint,
|
|
|
|
|
|
get_protected_keys_for_endpoint,
|
2026-02-25 21:59:29 +08:00
|
|
|
|
resolve_endpoint_definition,
|
2026-02-01 17:25:28 +08:00
|
|
|
|
)
|
|
|
|
|
|
from src.core.api_format.signature import EndpointSignature, parse_signature_key
|
2026-02-09 01:05:48 +08:00
|
|
|
|
from src.core.logger import logger
|
2026-01-15 22:25:56 +08:00
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 头部常量定义
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
2026-02-25 21:59:29 +08:00
|
|
|
|
# 通用浏览器指纹 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",
|
2026-02-28 20:53:11 +08:00
|
|
|
|
"Accept-Encoding": "gzip, deflate, br",
|
2026-02-25 21:59:29 +08:00
|
|
|
|
"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",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-09 01:05:48 +08:00
|
|
|
|
# 转发给上游时需要剔除的头部(系统管理 + 认证替换 + 客户端/代理元数据)
|
2026-01-30 03:10:21 +08:00
|
|
|
|
UPSTREAM_DROP_HEADERS: frozenset[str] = frozenset(
|
2026-01-15 22:25:56 +08:00
|
|
|
|
{
|
|
|
|
|
|
# 认证头 - 会被替换为 Provider 的认证
|
|
|
|
|
|
"authorization",
|
|
|
|
|
|
"x-api-key",
|
|
|
|
|
|
"x-goog-api-key",
|
|
|
|
|
|
# 系统管理头 - 由 HTTP 客户端重新生成
|
|
|
|
|
|
"host",
|
|
|
|
|
|
"content-length",
|
|
|
|
|
|
"transfer-encoding",
|
|
|
|
|
|
"connection",
|
2026-02-28 20:53:11 +08:00
|
|
|
|
# 编码头 - 丢弃客户端值,由 BROWSER_FINGERPRINT_HEADERS 统一设置
|
2026-01-15 22:25:56 +08:00
|
|
|
|
"accept-encoding",
|
2026-02-09 01:05:48 +08:00
|
|
|
|
# 反向代理 / 网关注入的头部 - 属于本站基础设施,不应泄露给上游
|
|
|
|
|
|
"x-real-ip",
|
|
|
|
|
|
"x-real-proto",
|
|
|
|
|
|
"x-forwarded-for",
|
|
|
|
|
|
"x-forwarded-proto",
|
|
|
|
|
|
"x-forwarded-host",
|
|
|
|
|
|
"x-forwarded-port",
|
2026-01-15 22:25:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 最小必脱敏集合(编译时常量,用于快速路径)
|
|
|
|
|
|
# 完整脱敏应使用 SystemConfigService.get_sensitive_headers()
|
2026-01-30 03:10:21 +08:00
|
|
|
|
CORE_REDACT_HEADERS: frozenset[str] = frozenset(
|
2026-01-15 22:25:56 +08:00
|
|
|
|
{
|
|
|
|
|
|
"authorization",
|
|
|
|
|
|
"x-api-key",
|
|
|
|
|
|
"x-goog-api-key",
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Hop-by-hop 头部 (RFC 7230)
|
2026-01-30 03:10:21 +08:00
|
|
|
|
HOP_BY_HOP_HEADERS: frozenset[str] = frozenset(
|
2026-01-15 22:25:56 +08:00
|
|
|
|
{
|
|
|
|
|
|
"connection",
|
|
|
|
|
|
"keep-alive",
|
|
|
|
|
|
"proxy-authenticate",
|
|
|
|
|
|
"proxy-authorization",
|
|
|
|
|
|
"te",
|
|
|
|
|
|
"trailer",
|
|
|
|
|
|
"transfer-encoding",
|
|
|
|
|
|
"upgrade",
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 响应时需要过滤的头部(body-dependent + hop-by-hop)
|
2026-01-30 03:10:21 +08:00
|
|
|
|
RESPONSE_DROP_HEADERS: frozenset[str] = (
|
2026-01-15 22:25:56 +08:00
|
|
|
|
frozenset(
|
|
|
|
|
|
{
|
|
|
|
|
|
"content-length",
|
|
|
|
|
|
"content-encoding",
|
|
|
|
|
|
"transfer-encoding",
|
|
|
|
|
|
"content-type",
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
| HOP_BY_HOP_HEADERS
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 请求头规范化
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def normalize_headers(headers: dict[str, str]) -> dict[str, str]:
|
2026-01-15 22:25:56 +08:00
|
|
|
|
"""
|
|
|
|
|
|
将请求头 key 统一为小写
|
|
|
|
|
|
|
|
|
|
|
|
用于处理 context.original_headers 的大小写敏感问题。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
return {k.lower(): v for k, v in headers.items()}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def get_header_value(headers: dict[str, str], key: str, default: str = "") -> str:
|
2026-01-15 22:25:56 +08:00
|
|
|
|
"""
|
|
|
|
|
|
大小写不敏感地获取请求头值
|
|
|
|
|
|
|
|
|
|
|
|
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 提取
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-01 17:25:28 +08:00
|
|
|
|
def extract_client_api_key_for_endpoint(
|
|
|
|
|
|
headers: dict[str, str],
|
|
|
|
|
|
endpoint: str | EndpointSignature | tuple,
|
|
|
|
|
|
) -> str | None:
|
2026-01-15 22:25:56 +08:00
|
|
|
|
"""
|
2026-02-01 17:25:28 +08:00
|
|
|
|
新模式:从客户端请求头提取 API Key。
|
2026-01-15 22:25:56 +08:00
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
headers: 原始请求头(自动处理大小写)
|
2026-02-01 17:25:28 +08:00
|
|
|
|
endpoint: endpoint signature(`family:kind` / EndpointSignature / (ApiFamily, EndpointKind))
|
2026-01-15 22:25:56 +08:00
|
|
|
|
"""
|
2026-02-01 17:25:28 +08:00
|
|
|
|
auth_header, auth_type = get_auth_config_for_endpoint(endpoint)
|
2026-01-15 22:25:56 +08:00
|
|
|
|
value = get_header_value(headers, auth_header)
|
|
|
|
|
|
if not value:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
if auth_type == "bearer":
|
|
|
|
|
|
if value.lower().startswith("bearer "):
|
2026-02-01 17:25:28 +08:00
|
|
|
|
return value[7:]
|
2026-01-15 22:25:56 +08:00
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-01 17:25:28 +08:00
|
|
|
|
def extract_client_api_key_for_endpoint_with_query(
|
2026-01-30 03:10:21 +08:00
|
|
|
|
headers: dict[str, str],
|
|
|
|
|
|
query_params: dict[str, str] | None,
|
2026-02-01 17:25:28 +08:00
|
|
|
|
endpoint: str | EndpointSignature | tuple,
|
2026-01-30 03:10:21 +08:00
|
|
|
|
) -> str | None:
|
2026-01-22 19:11:38 +08:00
|
|
|
|
"""
|
2026-02-01 17:25:28 +08:00
|
|
|
|
新模式:从客户端请求头或 URL 参数提取 API Key。
|
2026-01-22 19:11:38 +08:00
|
|
|
|
|
2026-02-01 17:25:28 +08:00
|
|
|
|
Gemini family 优先级:
|
2026-01-22 19:11:38 +08:00
|
|
|
|
1. URL 参数 ?key=
|
|
|
|
|
|
2. x-goog-api-key 请求头
|
|
|
|
|
|
"""
|
2026-02-01 17:25:28 +08:00
|
|
|
|
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":
|
2026-01-22 19:11:38 +08:00
|
|
|
|
query_key = query_params.get("key") if query_params else None
|
|
|
|
|
|
if query_key:
|
|
|
|
|
|
return query_key
|
|
|
|
|
|
|
2026-02-01 17:25:28 +08:00
|
|
|
|
return extract_client_api_key_for_endpoint(headers, endpoint)
|
2026-01-22 19:11:38 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-01-15 22:25:56 +08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 能力需求检测
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-01 17:25:28 +08:00
|
|
|
|
def detect_capabilities_for_endpoint(
|
2026-01-30 03:10:21 +08:00
|
|
|
|
headers: dict[str, str],
|
2026-02-01 17:25:28 +08:00
|
|
|
|
endpoint: str | EndpointSignature | tuple,
|
|
|
|
|
|
request_body: dict[str, Any] | None = None, # noqa: ARG001 - 预留
|
2026-01-30 03:10:21 +08:00
|
|
|
|
) -> dict[str, bool]:
|
2026-01-15 22:25:56 +08:00
|
|
|
|
"""
|
2026-02-01 17:25:28 +08:00
|
|
|
|
新模式:从请求头检测能力需求。
|
2026-01-15 22:25:56 +08:00
|
|
|
|
|
2026-02-01 17:25:28 +08:00
|
|
|
|
当前支持:
|
|
|
|
|
|
- Claude family: anthropic-beta 头中的 context-1m
|
2026-01-15 22:25:56 +08:00
|
|
|
|
"""
|
2026-01-30 03:10:21 +08:00
|
|
|
|
requirements: dict[str, bool] = {}
|
2026-01-15 22:25:56 +08:00
|
|
|
|
|
2026-02-01 17:25:28 +08:00
|
|
|
|
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":
|
2026-01-15 22:25:56 +08:00
|
|
|
|
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)
|
2026-01-30 03:10:21 +08:00
|
|
|
|
self._headers: dict[str, tuple[str, str]] = {}
|
2026-01-15 22:25:56 +08:00
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def add(self, key: str, value: str) -> HeaderBuilder:
|
2026-01-15 22:25:56 +08:00
|
|
|
|
"""添加单个头部(会覆盖同名头部)"""
|
|
|
|
|
|
self._headers[key.lower()] = (key, value)
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def add_many(self, headers: dict[str, str]) -> HeaderBuilder:
|
2026-01-15 22:25:56 +08:00
|
|
|
|
"""批量添加头部"""
|
|
|
|
|
|
for k, v in headers.items():
|
|
|
|
|
|
self.add(k, v)
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
2026-02-01 17:25:28 +08:00
|
|
|
|
def add_protected(
|
|
|
|
|
|
self, headers: dict[str, str], protected_keys: AbstractSet[str]
|
|
|
|
|
|
) -> HeaderBuilder:
|
2026-01-15 22:25:56 +08:00
|
|
|
|
"""
|
|
|
|
|
|
添加头部但保护指定的 key 不被覆盖
|
|
|
|
|
|
|
2026-01-16 01:18:54 +08:00
|
|
|
|
用于 endpoint 额外请求头不能覆盖认证头的场景。
|
2026-01-15 22:25:56 +08:00
|
|
|
|
"""
|
|
|
|
|
|
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
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def remove(self, keys: frozenset[str]) -> HeaderBuilder:
|
2026-01-15 22:25:56 +08:00
|
|
|
|
"""移除指定的头部"""
|
|
|
|
|
|
for k in keys:
|
|
|
|
|
|
self._headers.pop(k.lower(), None)
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def rename(self, from_key: str, to_key: str) -> HeaderBuilder:
|
2026-01-16 01:18:54 +08:00
|
|
|
|
"""
|
|
|
|
|
|
重命名头部(保留原值)
|
|
|
|
|
|
|
|
|
|
|
|
如果 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,
|
2026-01-30 03:10:21 +08:00
|
|
|
|
rules: list[dict[str, Any]],
|
|
|
|
|
|
protected_keys: AbstractSet[str] | None = None,
|
|
|
|
|
|
) -> HeaderBuilder:
|
2026-01-16 01:18:54 +08:00
|
|
|
|
"""
|
|
|
|
|
|
应用请求头规则
|
|
|
|
|
|
|
|
|
|
|
|
支持的规则类型:
|
|
|
|
|
|
- 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 都不能是受保护的
|
2026-02-01 17:25:28 +08:00
|
|
|
|
if (
|
|
|
|
|
|
from_key.lower() not in protected_lower
|
|
|
|
|
|
and to_key.lower() not in protected_lower
|
|
|
|
|
|
):
|
2026-01-16 01:18:54 +08:00
|
|
|
|
self.rename(from_key, to_key)
|
|
|
|
|
|
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def build(self) -> dict[str, str]:
|
2026-02-26 02:56:54 +08:00
|
|
|
|
"""构建最终的头部字典"""
|
2026-02-09 01:05:48 +08:00
|
|
|
|
result: dict[str, str] = {}
|
|
|
|
|
|
for original_key, value in self._headers.values():
|
2026-02-26 02:56:54 +08:00
|
|
|
|
result[original_key] = _normalize_header_value_for_httpx(original_key, value)
|
2026-02-09 01:05:48 +08:00
|
|
|
|
return result
|
2026-01-15 22:25:56 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-02-26 02:56:54 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-01 17:25:28 +08:00
|
|
|
|
def build_upstream_headers_for_endpoint(
|
2026-01-30 03:10:21 +08:00
|
|
|
|
original_headers: dict[str, str],
|
2026-02-01 17:25:28 +08:00
|
|
|
|
endpoint: str | EndpointSignature | tuple,
|
2026-01-15 22:25:56 +08:00
|
|
|
|
provider_api_key: str,
|
|
|
|
|
|
*,
|
2026-01-30 03:10:21 +08:00
|
|
|
|
endpoint_headers: dict[str, str] | None = None,
|
|
|
|
|
|
extra_headers: dict[str, str] | None = None,
|
|
|
|
|
|
drop_headers: frozenset[str] | None = None,
|
2026-02-06 16:37:06 +08:00
|
|
|
|
header_rules: list[dict[str, Any]] | None = None,
|
2026-01-30 03:10:21 +08:00
|
|
|
|
) -> dict[str, str]:
|
2026-01-15 22:25:56 +08:00
|
|
|
|
"""
|
2026-02-01 17:25:28 +08:00
|
|
|
|
新模式:构建发送给上游 Provider 的请求头(基于 endpoint signature)。
|
2026-01-15 22:25:56 +08:00
|
|
|
|
|
|
|
|
|
|
优先级(后者覆盖前者):
|
|
|
|
|
|
1. 原始头部(排除 drop_headers)
|
|
|
|
|
|
2. endpoint 配置头部
|
2026-02-06 16:37:06 +08:00
|
|
|
|
3. header_rules(用户自定义的请求头规则,支持 set/drop/rename)
|
|
|
|
|
|
4. extra_headers
|
|
|
|
|
|
5. 认证头(最高优先级,始终设置)
|
2026-01-15 22:25:56 +08:00
|
|
|
|
"""
|
|
|
|
|
|
if drop_headers is None:
|
|
|
|
|
|
drop_headers = UPSTREAM_DROP_HEADERS
|
|
|
|
|
|
|
2026-02-01 17:25:28 +08:00
|
|
|
|
auth_header, auth_type = get_auth_config_for_endpoint(endpoint)
|
2026-01-15 22:25:56 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
2026-02-06 16:37:06 +08:00
|
|
|
|
# 应用用户自定义的请求头规则(认证头受保护)
|
|
|
|
|
|
if header_rules:
|
|
|
|
|
|
builder.apply_rules(header_rules, protected_keys)
|
|
|
|
|
|
|
2026-01-15 22:25:56 +08:00
|
|
|
|
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(
|
2026-01-30 03:10:21 +08:00
|
|
|
|
base_headers: dict[str, str],
|
|
|
|
|
|
extra_headers: dict[str, str] | None,
|
|
|
|
|
|
protected_keys: frozenset[str] | set[str],
|
|
|
|
|
|
) -> dict[str, str]:
|
2026-01-15 22:25:56 +08:00
|
|
|
|
"""
|
|
|
|
|
|
合并头部但保护指定的 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(
|
2026-01-30 03:10:21 +08:00
|
|
|
|
headers: dict[str, str] | None,
|
|
|
|
|
|
drop_headers: frozenset[str] | None = None,
|
|
|
|
|
|
) -> dict[str, str]:
|
2026-01-15 22:25:56 +08:00
|
|
|
|
"""
|
|
|
|
|
|
过滤上游响应头中不应透传给客户端的字段
|
|
|
|
|
|
|
|
|
|
|
|
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(
|
2026-01-30 03:10:21 +08:00
|
|
|
|
headers: dict[str, str],
|
|
|
|
|
|
redact_keys: frozenset[str] | None = None,
|
|
|
|
|
|
) -> dict[str, str]:
|
2026-01-15 22:25:56 +08:00
|
|
|
|
"""
|
|
|
|
|
|
将敏感头部值替换为 *** 用于日志记录
|
|
|
|
|
|
|
|
|
|
|
|
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 统一接口
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-01 17:25:28 +08:00
|
|
|
|
def build_adapter_base_headers_for_endpoint(
|
|
|
|
|
|
endpoint: str | EndpointSignature | tuple,
|
2026-01-15 22:25:56 +08:00
|
|
|
|
api_key: str,
|
|
|
|
|
|
*,
|
|
|
|
|
|
include_extra: bool = True,
|
2026-01-30 03:10:21 +08:00
|
|
|
|
) -> dict[str, str]:
|
2026-01-15 22:25:56 +08:00
|
|
|
|
"""
|
2026-02-01 17:25:28 +08:00
|
|
|
|
新模式:根据 endpoint signature 构建基础请求头。
|
2026-02-25 21:59:29 +08:00
|
|
|
|
|
|
|
|
|
|
浏览器指纹 headers 作为底层默认值注入,Claude API family 额外注入 Anthropic 专属 header。
|
|
|
|
|
|
认证头和 extra_headers 会覆盖它们。
|
2026-01-15 22:25:56 +08:00
|
|
|
|
"""
|
2026-02-01 17:25:28 +08:00
|
|
|
|
auth_header, auth_type = get_auth_config_for_endpoint(endpoint)
|
2026-01-15 22:25:56 +08:00
|
|
|
|
auth_value = f"Bearer {api_key}" if auth_type == "bearer" else api_key
|
|
|
|
|
|
|
2026-02-25 21:59:29 +08:00
|
|
|
|
# 以浏览器指纹为底层默认值,绕过 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"
|
2026-01-15 22:25:56 +08:00
|
|
|
|
|
|
|
|
|
|
if include_extra:
|
2026-02-01 17:25:28 +08:00
|
|
|
|
extra = get_extra_headers_for_endpoint(endpoint)
|
2026-01-15 22:25:56 +08:00
|
|
|
|
if extra:
|
|
|
|
|
|
headers.update(extra)
|
|
|
|
|
|
|
|
|
|
|
|
return headers
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-01 17:25:28 +08:00
|
|
|
|
def build_adapter_headers_for_endpoint(
|
|
|
|
|
|
endpoint: str | EndpointSignature | tuple,
|
2026-01-15 22:25:56 +08:00
|
|
|
|
api_key: str,
|
2026-01-30 03:10:21 +08:00
|
|
|
|
extra_headers: dict[str, str] | None = None,
|
|
|
|
|
|
) -> dict[str, str]:
|
2026-01-15 22:25:56 +08:00
|
|
|
|
"""
|
2026-02-01 17:25:28 +08:00
|
|
|
|
新模式:构建完整的 Adapter 请求头(包含 extra_headers)。
|
2026-01-15 22:25:56 +08:00
|
|
|
|
"""
|
2026-02-01 17:25:28 +08:00
|
|
|
|
base = build_adapter_base_headers_for_endpoint(endpoint, api_key)
|
2026-01-15 22:25:56 +08:00
|
|
|
|
if not extra_headers:
|
|
|
|
|
|
return base
|
2026-02-01 17:25:28 +08:00
|
|
|
|
protected = get_protected_keys_for_endpoint(endpoint)
|
2026-01-15 22:25:56 +08:00
|
|
|
|
return merge_headers_with_protection(base, extra_headers, protected)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-01 17:25:28 +08:00
|
|
|
|
def get_adapter_protected_keys_for_endpoint(
|
|
|
|
|
|
endpoint: str | EndpointSignature | tuple,
|
|
|
|
|
|
) -> tuple[str, ...]:
|
|
|
|
|
|
"""新模式:获取 Adapter 的受保护头部 key。"""
|
|
|
|
|
|
return tuple(get_protected_keys_for_endpoint(endpoint))
|
2026-01-15 22:25:56 +08:00
|
|
|
|
|
2026-01-16 01:18:54 +08:00
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# Header Rules 工具函数
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def extract_set_headers_from_rules(
|
2026-01-30 03:10:21 +08:00
|
|
|
|
header_rules: list[dict[str, Any]] | None,
|
|
|
|
|
|
) -> dict[str, str] | None:
|
2026-01-16 01:18:54 +08:00
|
|
|
|
"""
|
|
|
|
|
|
从 header_rules 中提取 set 操作生成的头部字典
|
|
|
|
|
|
|
|
|
|
|
|
用于需要构造额外请求头的场景(如模型列表查询、模型测试等)。
|
|
|
|
|
|
注意:drop 和 rename 操作在这里不适用,因为它们用于修改已存在的头部。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
header_rules: 请求头规则列表 [{"action": "set", "key": "X-Custom", "value": "val"}, ...]
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
set 操作生成的头部字典,如果没有则返回 None
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not header_rules:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
headers: dict[str, str] = {}
|
2026-01-16 01:18:54 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def get_extra_headers_from_endpoint(endpoint: Any) -> dict[str, str] | None:
|
2026-01-16 01:18:54 +08:00
|
|
|
|
"""
|
|
|
|
|
|
从 endpoint 提取额外请求头
|
|
|
|
|
|
|
|
|
|
|
|
用于需要构造额外请求头的场景(如模型列表查询、模型测试等)。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
endpoint: ProviderEndpoint 对象
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
额外请求头字典,如果没有则返回 None
|
|
|
|
|
|
"""
|
|
|
|
|
|
header_rules = getattr(endpoint, "header_rules", None)
|
|
|
|
|
|
return extract_set_headers_from_rules(header_rules)
|
2026-02-09 01:05:48 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 请求头辅助工具
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|