mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
headers.py 删除 build_browser_fingerprint_headers / build_anthropic_extra_headers 函数及相关辅助,改为直接使用固定常量;PassthroughRequestBuilder 移除 Claude 格式的指纹注入步骤;Antigravity constants 移除 per-key 指纹 覆盖逻辑,统一使用进程级固定值。
674 lines
21 KiB
Python
674 lines
21 KiB
Python
"""
|
||
统一的请求头处理模块
|
||
|
||
职责:
|
||
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
|