mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
refactor: 统一请求头处理逻辑到 headers.py
- 新增 src/core/headers.py 集中管理头部处理函数 - 扩展 api_format_metadata.py 添加 extra_headers 和 protected_keys 配置 - 将各 adapter 中重复的头部方法提取到基类,统一调用 headers.py - 移除 transport.py 中未使用的 build_provider_headers 函数 - 添加 headers 模块的单元测试
This commit is contained in:
@@ -22,12 +22,13 @@ from abc import abstractmethod
|
||||
from typing import Any, Dict, Optional, Tuple, Type
|
||||
|
||||
import httpx
|
||||
from fastapi import HTTPException
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from src.api.base.adapter import ApiAdapter, ApiMode
|
||||
from src.api.base.context import ApiRequestContext
|
||||
from src.api.handlers.base.chat_handler_base import ChatHandlerBase
|
||||
from src.core.enums import APIFormat
|
||||
from src.core.exceptions import (
|
||||
InvalidRequestException,
|
||||
ModelNotSupportedException,
|
||||
@@ -39,6 +40,12 @@ from src.core.exceptions import (
|
||||
QuotaExceededException,
|
||||
UpstreamClientException,
|
||||
)
|
||||
from src.core.headers import (
|
||||
build_adapter_base_headers,
|
||||
build_adapter_headers,
|
||||
extract_client_api_key,
|
||||
get_adapter_protected_keys,
|
||||
)
|
||||
from src.core.logger import logger
|
||||
from src.services.billing import calculate_request_cost as _calculate_request_cost
|
||||
from src.services.request.result import RequestResult
|
||||
@@ -67,6 +74,14 @@ class ChatAdapterBase(ApiAdapter):
|
||||
# 计费模板配置(子类可覆盖,如 "claude", "openai", "gemini")
|
||||
BILLING_TEMPLATE: str = "claude"
|
||||
|
||||
@classmethod
|
||||
def _get_api_format(cls) -> APIFormat:
|
||||
"""获取 API 格式枚举,用于调用 headers.py 的统一函数"""
|
||||
try:
|
||||
return APIFormat[cls.FORMAT_ID]
|
||||
except KeyError:
|
||||
return APIFormat.OPENAI # 默认回退
|
||||
|
||||
# 子类可以配置的特殊方法(用于check_endpoint)
|
||||
@classmethod
|
||||
def build_endpoint_url(cls, base_url: str) -> str:
|
||||
@@ -76,18 +91,20 @@ class ChatAdapterBase(ApiAdapter):
|
||||
|
||||
@classmethod
|
||||
def build_base_headers(cls, api_key: str) -> Dict[str, str]:
|
||||
"""构建基础请求头,子类可以覆盖以自定义认证头"""
|
||||
# 默认实现:Bearer token认证
|
||||
return {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
"""构建基础请求头,使用统一的 headers.py 实现"""
|
||||
return build_adapter_base_headers(cls._get_api_format(), api_key)
|
||||
|
||||
@classmethod
|
||||
def get_protected_header_keys(cls) -> tuple:
|
||||
"""返回不应被extra_headers覆盖的头部key,子类可以覆盖"""
|
||||
# 默认保护认证相关头部
|
||||
return ("authorization", "content-type")
|
||||
"""返回不应被extra_headers覆盖的头部key,使用统一的 headers.py 实现"""
|
||||
return get_adapter_protected_keys(cls._get_api_format())
|
||||
|
||||
@classmethod
|
||||
def build_headers_with_extra(
|
||||
cls, api_key: str, extra_headers: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, str]:
|
||||
"""构建完整请求头(包含 extra_headers),使用统一的 headers.py 实现"""
|
||||
return build_adapter_headers(cls._get_api_format(), api_key, extra_headers)
|
||||
|
||||
@classmethod
|
||||
def build_request_body(cls, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
@@ -95,6 +112,10 @@ class ChatAdapterBase(ApiAdapter):
|
||||
# 默认实现:直接使用请求数据
|
||||
return request_data.copy()
|
||||
|
||||
def extract_api_key(self, request: Request) -> Optional[str]:
|
||||
"""从请求中提取 API 密钥,使用统一的 headers.py 实现"""
|
||||
return extract_client_api_key(dict(request.headers), self._get_api_format())
|
||||
|
||||
def __init__(self, allowed_api_formats: Optional[list[str]] = None):
|
||||
self.allowed_api_formats = allowed_api_formats or [self.FORMAT_ID]
|
||||
|
||||
@@ -626,13 +647,11 @@ class ChatAdapterBase(ApiAdapter):
|
||||
Returns:
|
||||
测试响应数据
|
||||
"""
|
||||
from src.api.handlers.base.endpoint_checker import build_safe_headers, run_endpoint_check
|
||||
from src.api.handlers.base.endpoint_checker import run_endpoint_check
|
||||
|
||||
# 使用子类配置方法构建请求组件
|
||||
url = cls.build_endpoint_url(base_url)
|
||||
base_headers = cls.build_base_headers(api_key)
|
||||
protected_keys = cls.get_protected_header_keys()
|
||||
headers = build_safe_headers(base_headers, extra_headers, protected_keys)
|
||||
headers = cls.build_headers_with_extra(api_key, extra_headers)
|
||||
body = cls.build_request_body(request_data)
|
||||
|
||||
# 使用通用的endpoint checker执行请求
|
||||
|
||||
@@ -20,12 +20,13 @@ import traceback
|
||||
from typing import Any, Dict, Optional, Tuple, Type
|
||||
|
||||
import httpx
|
||||
from fastapi import HTTPException
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from src.api.base.adapter import ApiAdapter, ApiMode
|
||||
from src.api.base.context import ApiRequestContext
|
||||
from src.api.handlers.base.cli_handler_base import CliMessageHandlerBase
|
||||
from src.core.enums import APIFormat
|
||||
from src.core.exceptions import (
|
||||
InvalidRequestException,
|
||||
ModelNotSupportedException,
|
||||
@@ -37,6 +38,12 @@ from src.core.exceptions import (
|
||||
QuotaExceededException,
|
||||
UpstreamClientException,
|
||||
)
|
||||
from src.core.headers import (
|
||||
build_adapter_base_headers,
|
||||
build_adapter_headers,
|
||||
extract_client_api_key,
|
||||
get_adapter_protected_keys,
|
||||
)
|
||||
from src.core.logger import logger
|
||||
from src.services.billing import calculate_request_cost as _calculate_request_cost
|
||||
from src.services.request.result import RequestResult
|
||||
@@ -68,6 +75,55 @@ class CliAdapterBase(ApiAdapter):
|
||||
def __init__(self, allowed_api_formats: Optional[list[str]] = None):
|
||||
self.allowed_api_formats = allowed_api_formats or [self.FORMAT_ID]
|
||||
|
||||
# =========================================================================
|
||||
# API 格式与头部处理 - 使用统一的 headers.py 函数
|
||||
# =========================================================================
|
||||
|
||||
@classmethod
|
||||
def _get_api_format(cls) -> APIFormat:
|
||||
"""将 FORMAT_ID 转换为 APIFormat 枚举"""
|
||||
try:
|
||||
return APIFormat[cls.FORMAT_ID]
|
||||
except KeyError:
|
||||
return APIFormat.OPENAI
|
||||
|
||||
def extract_api_key(self, request: Request) -> Optional[str]:
|
||||
"""
|
||||
从请求中提取 API 密钥
|
||||
|
||||
使用统一的头部处理函数,根据 API 格式自动识别认证头。
|
||||
"""
|
||||
return extract_client_api_key(dict(request.headers), self._get_api_format())
|
||||
|
||||
@classmethod
|
||||
def build_base_headers(cls, api_key: str) -> Dict[str, str]:
|
||||
"""
|
||||
构建 CLI API 认证头
|
||||
|
||||
使用统一的头部处理函数。
|
||||
"""
|
||||
return build_adapter_base_headers(cls._get_api_format(), api_key)
|
||||
|
||||
@classmethod
|
||||
def build_headers_with_extra(
|
||||
cls, api_key: str, extra_headers: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
构建带额外头部的完整请求头
|
||||
|
||||
使用统一的头部处理函数,自动保护关键头部不被覆盖。
|
||||
"""
|
||||
return build_adapter_headers(cls._get_api_format(), api_key, extra_headers)
|
||||
|
||||
@classmethod
|
||||
def get_protected_header_keys(cls) -> tuple[str, ...]:
|
||||
"""
|
||||
返回 CLI API 的保护头部 key
|
||||
|
||||
使用统一的头部处理函数。
|
||||
"""
|
||||
return get_adapter_protected_keys(cls._get_api_format())
|
||||
|
||||
async def handle(self, context: ApiRequestContext):
|
||||
"""处理 CLI API 请求"""
|
||||
http_request = context.request
|
||||
@@ -575,20 +631,19 @@ class CliAdapterBase(ApiAdapter):
|
||||
Returns:
|
||||
测试响应数据
|
||||
"""
|
||||
from src.api.handlers.base.endpoint_checker import build_safe_headers, run_endpoint_check
|
||||
from src.api.handlers.base.endpoint_checker import run_endpoint_check
|
||||
|
||||
# 构建请求组件
|
||||
url = cls.build_endpoint_url(base_url, request_data, model_name)
|
||||
base_headers = cls.build_base_headers(api_key)
|
||||
protected_keys = cls.get_protected_header_keys()
|
||||
|
||||
# 添加CLI User-Agent
|
||||
# 添加 CLI User-Agent 到 extra_headers
|
||||
cli_user_agent = cls.get_cli_user_agent()
|
||||
merged_extra = dict(extra_headers) if extra_headers else {}
|
||||
if cli_user_agent:
|
||||
base_headers["User-Agent"] = cli_user_agent
|
||||
protected_keys = tuple(list(protected_keys) + ["user-agent"])
|
||||
merged_extra["User-Agent"] = cli_user_agent
|
||||
|
||||
headers = build_safe_headers(base_headers, extra_headers, protected_keys)
|
||||
# 使用统一的头部构建函数
|
||||
headers = cls.build_headers_with_extra(api_key, merged_extra if merged_extra else None)
|
||||
body = cls.build_request_body(request_data)
|
||||
|
||||
# 获取有效的模型名称
|
||||
@@ -610,7 +665,7 @@ class CliAdapterBase(ApiAdapter):
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# CLI Adapter 配置方法 - 子类应覆盖这些方法而不是整个 check_endpoint
|
||||
# CLI Adapter 配置方法 - 子类应覆盖这些方法
|
||||
# =========================================================================
|
||||
|
||||
@classmethod
|
||||
@@ -628,29 +683,6 @@ class CliAdapterBase(ApiAdapter):
|
||||
"""
|
||||
raise NotImplementedError(f"{cls.FORMAT_ID} adapter must implement build_endpoint_url")
|
||||
|
||||
@classmethod
|
||||
def build_base_headers(cls, api_key: str) -> Dict[str, str]:
|
||||
"""
|
||||
构建CLI API认证头 - 子类应覆盖
|
||||
|
||||
Args:
|
||||
api_key: API密钥
|
||||
|
||||
Returns:
|
||||
基础认证头部字典
|
||||
"""
|
||||
raise NotImplementedError(f"{cls.FORMAT_ID} adapter must implement build_base_headers")
|
||||
|
||||
@classmethod
|
||||
def get_protected_header_keys(cls) -> tuple:
|
||||
"""
|
||||
返回CLI API的保护头部key - 子类应覆盖
|
||||
|
||||
Returns:
|
||||
保护头部key的元组
|
||||
"""
|
||||
raise NotImplementedError(f"{cls.FORMAT_ID} adapter must implement get_protected_header_keys")
|
||||
|
||||
@classmethod
|
||||
def build_request_body(cls, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
|
||||
@@ -27,23 +27,11 @@ from collections import defaultdict
|
||||
import httpx
|
||||
|
||||
from src.core.logger import logger
|
||||
|
||||
|
||||
_SENSITIVE_HEADER_KEYS = {
|
||||
"authorization",
|
||||
"x-api-key",
|
||||
"x-goog-api-key",
|
||||
}
|
||||
from src.core.headers import CORE_REDACT_HEADERS, merge_headers_with_protection, redact_headers_for_log
|
||||
|
||||
|
||||
def _redact_headers(headers: Dict[str, str]) -> Dict[str, str]:
|
||||
redacted: Dict[str, str] = {}
|
||||
for key, value in headers.items():
|
||||
if key.lower() in _SENSITIVE_HEADER_KEYS:
|
||||
redacted[key] = "***"
|
||||
else:
|
||||
redacted[key] = value
|
||||
return redacted
|
||||
return redact_headers_for_log(headers, CORE_REDACT_HEADERS)
|
||||
|
||||
|
||||
def _truncate_repr(value: Any, limit: int = 1200) -> str:
|
||||
@@ -64,14 +52,7 @@ def build_safe_headers(
|
||||
"""
|
||||
合并 extra_headers,但防止覆盖 protected_keys(大小写不敏感)。
|
||||
"""
|
||||
headers = dict(base_headers)
|
||||
if not extra_headers:
|
||||
return headers
|
||||
|
||||
protected = {k.lower() for k in protected_keys}
|
||||
safe_headers = {k: v for k, v in extra_headers.items() if k.lower() not in protected}
|
||||
headers.update(safe_headers)
|
||||
return headers
|
||||
return merge_headers_with_protection(base_headers, extra_headers, set(protected_keys))
|
||||
|
||||
|
||||
async def run_endpoint_check(
|
||||
|
||||
@@ -17,27 +17,14 @@ from abc import ABC, abstractmethod
|
||||
from typing import Any, Dict, FrozenSet, Optional, Tuple
|
||||
|
||||
from src.core.crypto import crypto_service
|
||||
from src.core.headers import HeaderBuilder, UPSTREAM_DROP_HEADERS
|
||||
|
||||
# ==============================================================================
|
||||
# 统一的头部配置常量
|
||||
# ==============================================================================
|
||||
|
||||
# 敏感头部 - 透传时需要清理(黑名单)
|
||||
# 这些头部要么包含认证信息,要么由代理层重新生成
|
||||
SENSITIVE_HEADERS: FrozenSet[str] = frozenset(
|
||||
{
|
||||
"authorization",
|
||||
"x-api-key",
|
||||
"x-goog-api-key", # Gemini API 认证头
|
||||
"host",
|
||||
"content-length",
|
||||
"transfer-encoding",
|
||||
"connection",
|
||||
# 不透传 accept-encoding,让 httpx 自己协商压缩格式
|
||||
# 避免客户端请求 brotli/zstd 但 httpx 不支持解压的问题
|
||||
"accept-encoding",
|
||||
}
|
||||
)
|
||||
# 兼容别名:历史代码使用 SENSITIVE_HEADERS 命名
|
||||
SENSITIVE_HEADERS: FrozenSet[str] = UPSTREAM_DROP_HEADERS
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
@@ -140,8 +127,6 @@ class PassthroughRequestBuilder(RequestBuilder):
|
||||
"""
|
||||
from src.core.api_format_metadata import get_auth_config, resolve_api_format
|
||||
|
||||
headers: Dict[str, str] = {}
|
||||
|
||||
# 1. 根据 API 格式自动设置认证头
|
||||
decrypted_key = crypto_service.decrypt(key.api_key)
|
||||
api_format = getattr(endpoint, "api_format", None)
|
||||
@@ -150,32 +135,32 @@ class PassthroughRequestBuilder(RequestBuilder):
|
||||
get_auth_config(resolved_format) if resolved_format else ("Authorization", "bearer")
|
||||
)
|
||||
|
||||
if auth_type == "bearer":
|
||||
headers[auth_header] = f"Bearer {decrypted_key}"
|
||||
else:
|
||||
headers[auth_header] = decrypted_key
|
||||
auth_value = f"Bearer {decrypted_key}" if auth_type == "bearer" else decrypted_key
|
||||
protected_keys = {auth_header.lower(), "content-type"}
|
||||
|
||||
# 2. 添加 endpoint 配置的额外头部
|
||||
if endpoint.headers:
|
||||
headers.update(endpoint.headers)
|
||||
builder = HeaderBuilder()
|
||||
|
||||
# 3. 透传原始头部(排除敏感头部 - 黑名单模式)
|
||||
# 2. 透传原始头部(排除敏感头部 - 黑名单模式)
|
||||
if original_headers:
|
||||
for name, value in original_headers.items():
|
||||
lower_name = name.lower()
|
||||
|
||||
# 跳过敏感头部
|
||||
if lower_name in SENSITIVE_HEADERS:
|
||||
if name.lower() in SENSITIVE_HEADERS:
|
||||
continue
|
||||
builder.add(name, value)
|
||||
|
||||
headers[name] = value
|
||||
# 3. 添加 endpoint 配置的额外头部(不能覆盖认证头/Content-Type)
|
||||
if endpoint.headers:
|
||||
builder.add_protected(endpoint.headers, protected_keys)
|
||||
|
||||
# 4. 添加额外头部
|
||||
if extra_headers:
|
||||
headers.update(extra_headers)
|
||||
builder.add_many(extra_headers)
|
||||
|
||||
# 5. 确保有 Content-Type
|
||||
if "Content-Type" not in headers and "content-type" not in headers:
|
||||
# 5. 设置认证头(最高优先级)
|
||||
builder.add(auth_header, auth_value)
|
||||
|
||||
# 6. 确保有 Content-Type
|
||||
headers = builder.build()
|
||||
if not any(k.lower() == "content-type" for k in headers):
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
return headers
|
||||
|
||||
@@ -6,6 +6,7 @@ import json
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from src.core.exceptions import EmbeddedErrorException, ProviderNotAvailableException
|
||||
from src.core.headers import filter_response_headers
|
||||
from src.core.logger import logger
|
||||
|
||||
|
||||
@@ -94,25 +95,6 @@ def build_sse_headers(extra_headers: Optional[Dict[str, str]] = None) -> Dict[st
|
||||
return headers
|
||||
|
||||
|
||||
_PROXY_RESPONSE_HEADER_BLOCKLIST = frozenset(
|
||||
{
|
||||
# Body-dependent headers: 我们会重编码响应体(JSONResponse / SSE),不能透传上游值
|
||||
"content-length",
|
||||
"content-encoding",
|
||||
"transfer-encoding",
|
||||
"content-type",
|
||||
# Hop-by-hop headers (RFC 7230)
|
||||
"connection",
|
||||
"keep-alive",
|
||||
"proxy-authenticate",
|
||||
"proxy-authorization",
|
||||
"te",
|
||||
"trailer",
|
||||
"upgrade",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def filter_proxy_response_headers(headers: Optional[Dict[str, str]]) -> Dict[str, str]:
|
||||
"""
|
||||
过滤上游响应头中不应透传给客户端的字段。
|
||||
@@ -123,9 +105,7 @@ def filter_proxy_response_headers(headers: Optional[Dict[str, str]]) -> Dict[str
|
||||
|
||||
如果透传上游的 `content-length/content-encoding/...`,会导致客户端解码失败或等待更多字节。
|
||||
"""
|
||||
if not headers:
|
||||
return {}
|
||||
return {k: v for k, v in headers.items() if k.lower() not in _PROXY_RESPONSE_HEADER_BLOCKLIST}
|
||||
return filter_response_headers(headers)
|
||||
|
||||
|
||||
def check_html_response(line: str) -> bool:
|
||||
|
||||
@@ -14,6 +14,7 @@ from src.api.base.adapter import ApiAdapter, ApiMode
|
||||
from src.api.base.context import ApiRequestContext
|
||||
from src.api.handlers.base.chat_adapter_base import ChatAdapterBase, register_adapter
|
||||
from src.api.handlers.base.chat_handler_base import ChatHandlerBase
|
||||
from src.core.headers import get_header_value
|
||||
from src.core.logger import logger
|
||||
from src.core.optimization_utils import TokenCounter
|
||||
from src.models.claude import ClaudeMessagesRequest, ClaudeTokenCountRequest
|
||||
@@ -39,17 +40,10 @@ class ClaudeCapabilityDetector:
|
||||
"""
|
||||
requirements: Dict[str, bool] = {}
|
||||
|
||||
# 检查 anthropic-beta 请求头(大小写不敏感)
|
||||
beta_header = None
|
||||
for key, value in headers.items():
|
||||
if key.lower() == "anthropic-beta":
|
||||
beta_header = value
|
||||
break
|
||||
|
||||
if beta_header:
|
||||
# 检查是否包含 context-1m 标识
|
||||
if "context-1m" in beta_header.lower():
|
||||
requirements["context_1m"] = True
|
||||
# 使用统一的大小写不敏感获取
|
||||
beta_header = get_header_value(headers, "anthropic-beta")
|
||||
if beta_header and "context-1m" in beta_header.lower():
|
||||
requirements["context_1m"] = True
|
||||
|
||||
return requirements
|
||||
|
||||
@@ -77,10 +71,6 @@ class ClaudeChatAdapter(ChatAdapterBase):
|
||||
super().__init__(allowed_api_formats or ["CLAUDE"])
|
||||
logger.info(f"[{self.name}] 初始化Chat模式适配器 | API格式: {self.allowed_api_formats}")
|
||||
|
||||
def extract_api_key(self, request: Request) -> Optional[str]:
|
||||
"""从请求中提取 API 密钥 (x-api-key)"""
|
||||
return request.headers.get("x-api-key")
|
||||
|
||||
def detect_capability_requirements(
|
||||
self,
|
||||
headers: Dict[str, str],
|
||||
@@ -166,18 +156,7 @@ class ClaudeChatAdapter(ChatAdapterBase):
|
||||
extra_headers: Optional[Dict[str, str]] = None,
|
||||
) -> Tuple[list, Optional[str]]:
|
||||
"""查询 Claude API 支持的模型列表"""
|
||||
headers = {
|
||||
"x-api-key": api_key,
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"anthropic-version": "2023-06-01",
|
||||
}
|
||||
if extra_headers:
|
||||
# 防止 extra_headers 覆盖认证头
|
||||
safe_headers = {
|
||||
k: v for k, v in extra_headers.items()
|
||||
if k.lower() not in ("x-api-key", "authorization", "anthropic-version")
|
||||
}
|
||||
headers.update(safe_headers)
|
||||
headers = cls.build_headers_with_extra(api_key, extra_headers)
|
||||
|
||||
# 构建 /v1/models URL
|
||||
base_url = base_url.rstrip("/")
|
||||
@@ -219,20 +198,6 @@ class ClaudeChatAdapter(ChatAdapterBase):
|
||||
else:
|
||||
return f"{base_url}/v1/messages"
|
||||
|
||||
@classmethod
|
||||
def build_base_headers(cls, api_key: str) -> Dict[str, str]:
|
||||
"""构建Claude API认证头"""
|
||||
return {
|
||||
"x-api-key": api_key,
|
||||
"Content-Type": "application/json",
|
||||
"anthropic-version": "2023-06-01",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_protected_header_keys(cls) -> tuple:
|
||||
"""返回Claude API的保护头部key"""
|
||||
return ("x-api-key", "content-type", "anthropic-version")
|
||||
|
||||
@classmethod
|
||||
def build_request_body(cls, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""构建Claude API请求体"""
|
||||
|
||||
@@ -4,10 +4,9 @@ Claude CLI Adapter - 基于通用 CLI Adapter 基类的简化实现
|
||||
继承 CliAdapterBase,只需配置 FORMAT_ID 和 HANDLER_CLASS。
|
||||
"""
|
||||
|
||||
from typing import Any, AsyncIterator, Dict, Optional, Tuple, Type, Union
|
||||
from typing import Any, Dict, Optional, Tuple, Type
|
||||
|
||||
import httpx
|
||||
from fastapi import Request
|
||||
|
||||
from src.api.handlers.base.cli_adapter_base import CliAdapterBase, register_cli_adapter
|
||||
from src.api.handlers.base.cli_handler_base import CliMessageHandlerBase
|
||||
@@ -37,13 +36,6 @@ class ClaudeCliAdapter(CliAdapterBase):
|
||||
def __init__(self, allowed_api_formats: Optional[list[str]] = None):
|
||||
super().__init__(allowed_api_formats or ["CLAUDE_CLI"])
|
||||
|
||||
def extract_api_key(self, request: Request) -> Optional[str]:
|
||||
"""从请求中提取 API 密钥 (Authorization: Bearer)"""
|
||||
authorization = request.headers.get("authorization")
|
||||
if authorization and authorization.startswith("Bearer "):
|
||||
return authorization.replace("Bearer ", "")
|
||||
return None
|
||||
|
||||
def detect_capability_requirements(
|
||||
self,
|
||||
headers: Dict[str, str],
|
||||
@@ -136,19 +128,6 @@ class ClaudeCliAdapter(CliAdapterBase):
|
||||
else:
|
||||
return f"{base_url}/v1/messages"
|
||||
|
||||
@classmethod
|
||||
def build_base_headers(cls, api_key: str) -> Dict[str, str]:
|
||||
"""构建Claude CLI API认证头"""
|
||||
return {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_protected_header_keys(cls) -> tuple:
|
||||
"""返回Claude CLI API的保护头部key"""
|
||||
return ("authorization", "content-type")
|
||||
|
||||
@classmethod
|
||||
def build_request_body(cls, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""构建Claude CLI API请求体"""
|
||||
|
||||
@@ -4,15 +4,14 @@ Gemini Chat Adapter
|
||||
处理 Gemini API 格式的请求适配
|
||||
"""
|
||||
|
||||
from typing import Any, AsyncIterator, Dict, Optional, Tuple, Type, Union
|
||||
from typing import Any, Dict, Optional, Tuple, Type
|
||||
|
||||
import httpx
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi import HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from src.api.handlers.base.chat_adapter_base import ChatAdapterBase, register_adapter
|
||||
from src.api.handlers.base.chat_handler_base import ChatHandlerBase
|
||||
from src.api.handlers.base.endpoint_checker import build_safe_headers, run_endpoint_check
|
||||
from src.core.logger import logger
|
||||
from src.models.gemini import GeminiRequest
|
||||
|
||||
@@ -41,10 +40,6 @@ class GeminiChatAdapter(ChatAdapterBase):
|
||||
super().__init__(allowed_api_formats or ["GEMINI"])
|
||||
logger.info(f"[{self.name}] 初始化 Gemini Chat 适配器 | API格式: {self.allowed_api_formats}")
|
||||
|
||||
def extract_api_key(self, request: Request) -> Optional[str]:
|
||||
"""从请求中提取 API 密钥 (x-goog-api-key)"""
|
||||
return request.headers.get("x-goog-api-key")
|
||||
|
||||
def _merge_path_params(
|
||||
self, original_request_body: Dict[str, Any], path_params: Dict[str, Any] # noqa: ARG002
|
||||
) -> Dict[str, Any]:
|
||||
@@ -163,7 +158,7 @@ class GeminiChatAdapter(ChatAdapterBase):
|
||||
extra_headers: Optional[Dict[str, str]] = None,
|
||||
) -> Tuple[list, Optional[str]]:
|
||||
"""查询 Gemini API 支持的模型列表"""
|
||||
# 兼容 base_url 已包含 /v1beta 的情况
|
||||
# Gemini 使用 URL 参数传递 key,不需要 headers 中的认证
|
||||
base_url_clean = base_url.rstrip("/")
|
||||
if base_url_clean.endswith("/v1beta"):
|
||||
models_url = f"{base_url_clean}/models?key={api_key}"
|
||||
@@ -210,19 +205,6 @@ class GeminiChatAdapter(ChatAdapterBase):
|
||||
else:
|
||||
return f"{base_url}/v1beta"
|
||||
|
||||
@classmethod
|
||||
def build_base_headers(cls, api_key: str) -> Dict[str, str]:
|
||||
"""构建Gemini API认证头"""
|
||||
return {
|
||||
"x-goog-api-key": api_key,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_protected_header_keys(cls) -> tuple:
|
||||
"""返回Gemini API的保护头部key"""
|
||||
return ("x-goog-api-key", "content-type")
|
||||
|
||||
@classmethod
|
||||
def build_request_body(cls, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""构建Gemini API请求体"""
|
||||
@@ -263,13 +245,11 @@ class GeminiChatAdapter(ChatAdapterBase):
|
||||
}
|
||||
|
||||
# 使用基类配置方法,但重写URL构建逻辑
|
||||
base_url = cls.build_endpoint_url(base_url)
|
||||
url = f"{base_url}/models/{effective_model_name}:generateContent"
|
||||
base_url_resolved = cls.build_endpoint_url(base_url)
|
||||
url = f"{base_url_resolved}/models/{effective_model_name}:generateContent"
|
||||
|
||||
# 构建请求组件
|
||||
base_headers = cls.build_base_headers(api_key)
|
||||
protected_keys = cls.get_protected_header_keys()
|
||||
headers = build_safe_headers(base_headers, extra_headers, protected_keys)
|
||||
headers = cls.build_headers_with_extra(api_key, extra_headers)
|
||||
body = cls.build_request_body(request_data)
|
||||
|
||||
# 使用基类的通用endpoint checker
|
||||
@@ -290,7 +270,7 @@ class GeminiChatAdapter(ChatAdapterBase):
|
||||
)
|
||||
|
||||
|
||||
def build_gemini_adapter(x_app_header: str = "") -> GeminiChatAdapter:
|
||||
def build_gemini_adapter(x_app_header: str = "") -> GeminiChatAdapter: # noqa: ARG001
|
||||
"""
|
||||
根据请求头构建适当的 Gemini 适配器
|
||||
|
||||
|
||||
@@ -4,10 +4,9 @@ Gemini CLI Adapter - 基于通用 CLI Adapter 基类的实现
|
||||
继承 CliAdapterBase,处理 Gemini CLI 格式的请求。
|
||||
"""
|
||||
|
||||
from typing import Any, AsyncIterator, Dict, Optional, Tuple, Type, Union
|
||||
from typing import Any, Dict, Optional, Tuple, Type
|
||||
|
||||
import httpx
|
||||
from fastapi import Request
|
||||
|
||||
from src.api.handlers.base.cli_adapter_base import CliAdapterBase, register_cli_adapter
|
||||
from src.api.handlers.base.cli_handler_base import CliMessageHandlerBase
|
||||
@@ -37,10 +36,6 @@ class GeminiCliAdapter(CliAdapterBase):
|
||||
def __init__(self, allowed_api_formats: Optional[list[str]] = None):
|
||||
super().__init__(allowed_api_formats or ["GEMINI_CLI"])
|
||||
|
||||
def extract_api_key(self, request: Request) -> Optional[str]:
|
||||
"""从请求中提取 API 密钥 (x-goog-api-key)"""
|
||||
return request.headers.get("x-goog-api-key")
|
||||
|
||||
def _merge_path_params(
|
||||
self, original_request_body: Dict[str, Any], path_params: Dict[str, Any] # noqa: ARG002
|
||||
) -> Dict[str, Any]:
|
||||
@@ -138,19 +133,6 @@ class GeminiCliAdapter(CliAdapterBase):
|
||||
prefix = f"{base_url}/v1beta"
|
||||
return f"{prefix}/models/{effective_model_name}:generateContent"
|
||||
|
||||
@classmethod
|
||||
def build_base_headers(cls, api_key: str) -> Dict[str, str]:
|
||||
"""构建Gemini CLI API认证头"""
|
||||
return {
|
||||
"x-goog-api-key": api_key,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_protected_header_keys(cls) -> tuple:
|
||||
"""返回Gemini CLI API的保护头部key"""
|
||||
return ("x-goog-api-key", "content-type")
|
||||
|
||||
@classmethod
|
||||
def build_request_body(cls, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""构建Gemini CLI API请求体"""
|
||||
|
||||
@@ -4,14 +4,12 @@ OpenAI Chat Adapter - 基于 ChatAdapterBase 的 OpenAI Chat API 适配器
|
||||
处理 /v1/chat/completions 端点的 OpenAI Chat 格式请求。
|
||||
"""
|
||||
|
||||
from typing import Any, AsyncIterator, Dict, Optional, Tuple, Type, Union
|
||||
from typing import Any, Dict, Optional, Tuple, Type
|
||||
|
||||
import httpx
|
||||
from fastapi import Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from src.api.handlers.base.chat_adapter_base import ChatAdapterBase, register_adapter
|
||||
from src.api.handlers.base.endpoint_checker import build_safe_headers, run_endpoint_check
|
||||
from src.api.handlers.base.chat_handler_base import ChatHandlerBase
|
||||
from src.core.logger import logger
|
||||
from src.models.openai import OpenAIRequest
|
||||
@@ -39,13 +37,6 @@ class OpenAIChatAdapter(ChatAdapterBase):
|
||||
def __init__(self, allowed_api_formats: Optional[list[str]] = None):
|
||||
super().__init__(allowed_api_formats or ["OPENAI"])
|
||||
|
||||
def extract_api_key(self, request: Request) -> Optional[str]:
|
||||
"""从请求中提取 API 密钥 (Authorization: Bearer)"""
|
||||
authorization = request.headers.get("authorization")
|
||||
if authorization and authorization.startswith("Bearer "):
|
||||
return authorization.replace("Bearer ", "")
|
||||
return None
|
||||
|
||||
def _validate_request_body(self, original_request_body: dict, path_params: dict = None):
|
||||
"""验证请求体"""
|
||||
if not isinstance(original_request_body, dict):
|
||||
@@ -117,13 +108,7 @@ class OpenAIChatAdapter(ChatAdapterBase):
|
||||
extra_headers: Optional[Dict[str, str]] = None,
|
||||
) -> Tuple[list, Optional[str]]:
|
||||
"""查询 OpenAI 兼容 API 支持的模型列表"""
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
}
|
||||
if extra_headers:
|
||||
# 防止 extra_headers 覆盖 Authorization
|
||||
safe_headers = {k: v for k, v in extra_headers.items() if k.lower() != "authorization"}
|
||||
headers.update(safe_headers)
|
||||
headers = cls.build_headers_with_extra(api_key, extra_headers)
|
||||
|
||||
# 构建 /v1/models URL
|
||||
base_url = base_url.rstrip("/")
|
||||
@@ -165,23 +150,5 @@ class OpenAIChatAdapter(ChatAdapterBase):
|
||||
else:
|
||||
return f"{base_url}/v1/chat/completions"
|
||||
|
||||
@classmethod
|
||||
def build_base_headers(cls, api_key: str) -> Dict[str, str]:
|
||||
"""构建OpenAI API认证头"""
|
||||
return {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_protected_header_keys(cls) -> tuple:
|
||||
"""返回OpenAI API的保护头部key"""
|
||||
return ("authorization", "content-type")
|
||||
|
||||
@classmethod
|
||||
def build_request_body(cls, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""构建OpenAI API请求体"""
|
||||
return request_data.copy()
|
||||
|
||||
|
||||
__all__ = ["OpenAIChatAdapter"]
|
||||
|
||||
@@ -4,10 +4,9 @@ OpenAI CLI Adapter - 基于通用 CLI Adapter 基类的简化实现
|
||||
继承 CliAdapterBase,只需配置 FORMAT_ID 和 HANDLER_CLASS。
|
||||
"""
|
||||
|
||||
from typing import Any, AsyncIterator, Dict, Optional, Tuple, Type, Union
|
||||
from typing import Any, Dict, Optional, Tuple, Type
|
||||
|
||||
import httpx
|
||||
from fastapi import Request
|
||||
|
||||
from src.api.handlers.base.cli_adapter_base import CliAdapterBase, register_cli_adapter
|
||||
from src.api.handlers.base.cli_handler_base import CliMessageHandlerBase
|
||||
@@ -37,13 +36,6 @@ class OpenAICliAdapter(CliAdapterBase):
|
||||
def __init__(self, allowed_api_formats: Optional[list[str]] = None):
|
||||
super().__init__(allowed_api_formats or ["OPENAI_CLI"])
|
||||
|
||||
def extract_api_key(self, request: Request) -> Optional[str]:
|
||||
"""从请求中提取 API 密钥 (Authorization: Bearer)"""
|
||||
authorization = request.headers.get("authorization")
|
||||
if authorization and authorization.startswith("Bearer "):
|
||||
return authorization.replace("Bearer ", "")
|
||||
return None
|
||||
|
||||
# =========================================================================
|
||||
# 模型列表查询
|
||||
# =========================================================================
|
||||
@@ -78,19 +70,6 @@ class OpenAICliAdapter(CliAdapterBase):
|
||||
else:
|
||||
return f"{base_url}/v1/chat/completions"
|
||||
|
||||
@classmethod
|
||||
def build_base_headers(cls, api_key: str) -> Dict[str, str]:
|
||||
"""构建OpenAI CLI API认证头"""
|
||||
return {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_protected_header_keys(cls) -> tuple:
|
||||
"""返回OpenAI CLI API的保护头部key"""
|
||||
return ("authorization", "content-type")
|
||||
|
||||
@classmethod
|
||||
def build_request_body(cls, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""构建OpenAI CLI API请求体"""
|
||||
|
||||
@@ -36,6 +36,8 @@ class ApiFormatDefinition:
|
||||
- path_prefix: 本站路径前缀(如 /claude, /openai),为空表示无前缀
|
||||
- auth_header: 认证头名称 (如 "x-api-key", "x-goog-api-key")
|
||||
- auth_type: 认证类型 ("header" 直接放值, "bearer" 加 Bearer 前缀)
|
||||
- extra_headers: 该格式必须携带的额外头部(如 anthropic-version)
|
||||
- protected_keys: 不应被 extra_headers 覆盖的头部(小写)
|
||||
"""
|
||||
|
||||
api_format: APIFormat
|
||||
@@ -44,6 +46,8 @@ class ApiFormatDefinition:
|
||||
path_prefix: str = "" # 本站路径前缀,为空表示无前缀
|
||||
auth_header: str = "Authorization"
|
||||
auth_type: str = "bearer" # "bearer" or "header"
|
||||
extra_headers: Mapping[str, str] = field(default_factory=dict) # 格式必须的额外头部
|
||||
protected_keys: frozenset[str] = field(default_factory=frozenset) # 受保护的头部 key(小写)
|
||||
|
||||
def iter_aliases(self) -> Iterable[str]:
|
||||
"""返回大小写统一后的别名集合,包含枚举名本身。"""
|
||||
@@ -62,14 +66,17 @@ _DEFINITIONS: Dict[APIFormat, ApiFormatDefinition] = {
|
||||
path_prefix="", # 通过请求头区分格式,不使用路径前缀
|
||||
auth_header="x-api-key",
|
||||
auth_type="header",
|
||||
extra_headers={"anthropic-version": "2023-06-01"},
|
||||
protected_keys=frozenset({"x-api-key", "content-type", "anthropic-version"}),
|
||||
),
|
||||
APIFormat.CLAUDE_CLI: ApiFormatDefinition(
|
||||
api_format=APIFormat.CLAUDE_CLI,
|
||||
aliases=("claude_cli", "claude-cli"),
|
||||
default_path="/v1/messages",
|
||||
path_prefix="", # 与 CLAUDE 共享入口,通过 header 区分
|
||||
auth_header="authorization",
|
||||
auth_header="Authorization",
|
||||
auth_type="bearer",
|
||||
protected_keys=frozenset({"authorization", "content-type"}),
|
||||
),
|
||||
APIFormat.OPENAI: ApiFormatDefinition(
|
||||
api_format=APIFormat.OPENAI,
|
||||
@@ -88,6 +95,7 @@ _DEFINITIONS: Dict[APIFormat, ApiFormatDefinition] = {
|
||||
path_prefix="", # 默认格式
|
||||
auth_header="Authorization",
|
||||
auth_type="bearer",
|
||||
protected_keys=frozenset({"authorization", "content-type"}),
|
||||
),
|
||||
APIFormat.OPENAI_CLI: ApiFormatDefinition(
|
||||
api_format=APIFormat.OPENAI_CLI,
|
||||
@@ -96,6 +104,7 @@ _DEFINITIONS: Dict[APIFormat, ApiFormatDefinition] = {
|
||||
path_prefix="", # 与 OPENAI 共享入口
|
||||
auth_header="Authorization",
|
||||
auth_type="bearer",
|
||||
protected_keys=frozenset({"authorization", "content-type"}),
|
||||
),
|
||||
APIFormat.GEMINI: ApiFormatDefinition(
|
||||
api_format=APIFormat.GEMINI,
|
||||
@@ -104,6 +113,7 @@ _DEFINITIONS: Dict[APIFormat, ApiFormatDefinition] = {
|
||||
path_prefix="", # 通过请求头区分格式
|
||||
auth_header="x-goog-api-key",
|
||||
auth_type="header",
|
||||
protected_keys=frozenset({"x-goog-api-key", "content-type"}),
|
||||
),
|
||||
APIFormat.GEMINI_CLI: ApiFormatDefinition(
|
||||
api_format=APIFormat.GEMINI_CLI,
|
||||
@@ -112,6 +122,7 @@ _DEFINITIONS: Dict[APIFormat, ApiFormatDefinition] = {
|
||||
path_prefix="", # 与 GEMINI 共享入口
|
||||
auth_header="x-goog-api-key",
|
||||
auth_type="header",
|
||||
protected_keys=frozenset({"x-goog-api-key", "content-type"}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -180,6 +191,36 @@ def get_auth_config(api_format: APIFormat) -> tuple[str, str]:
|
||||
return "Authorization", "bearer"
|
||||
|
||||
|
||||
def get_extra_headers(api_format: APIFormat) -> Mapping[str, str]:
|
||||
"""
|
||||
获取该格式必须携带的额外头部。
|
||||
|
||||
例如 Claude 需要 anthropic-version 头部。
|
||||
|
||||
Returns:
|
||||
额外头部字典(只读)
|
||||
"""
|
||||
definition = API_FORMAT_DEFINITIONS.get(api_format)
|
||||
if definition:
|
||||
return definition.extra_headers
|
||||
return {}
|
||||
|
||||
|
||||
def get_protected_keys(api_format: APIFormat) -> frozenset[str]:
|
||||
"""
|
||||
获取该格式的受保护头部 key(小写)。
|
||||
|
||||
这些头部不应被 extra_headers 覆盖。
|
||||
|
||||
Returns:
|
||||
受保护的头部 key 集合
|
||||
"""
|
||||
definition = API_FORMAT_DEFINITIONS.get(api_format)
|
||||
if definition:
|
||||
return definition.protected_keys
|
||||
return frozenset({"authorization", "content-type"})
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _alias_lookup_cache() -> Dict[str, APIFormat]:
|
||||
"""缓存 alias -> APIFormat 查找表,减少重复构建。"""
|
||||
|
||||
473
src/core/headers.py
Normal file
473
src/core/headers.py
Normal file
@@ -0,0 +1,473 @@
|
||||
"""
|
||||
统一的请求头处理模块
|
||||
|
||||
职责:
|
||||
1. 请求头规范化(大小写统一)
|
||||
2. 客户端 API Key 提取
|
||||
3. 能力需求检测
|
||||
4. 上游请求头构建
|
||||
5. 响应头过滤
|
||||
6. 日志脱敏
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import AbstractSet, Any, Dict, FrozenSet, Optional, Set
|
||||
|
||||
from .api_format_metadata import get_auth_config, get_extra_headers, get_protected_keys
|
||||
from .enums import APIFormat
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 头部常量定义
|
||||
# =============================================================================
|
||||
|
||||
# 转发给上游时需要剔除的头部(系统管理 + 认证替换)
|
||||
UPSTREAM_DROP_HEADERS: FrozenSet[str] = frozenset(
|
||||
{
|
||||
# 认证头 - 会被替换为 Provider 的认证
|
||||
"authorization",
|
||||
"x-api-key",
|
||||
"x-goog-api-key",
|
||||
# 系统管理头 - 由 HTTP 客户端重新生成
|
||||
"host",
|
||||
"content-length",
|
||||
"transfer-encoding",
|
||||
"connection",
|
||||
# 编码头 - 避免客户端请求 brotli/zstd 但 httpx 不支持
|
||||
"accept-encoding",
|
||||
}
|
||||
)
|
||||
|
||||
# 最小必脱敏集合(编译时常量,用于快速路径)
|
||||
# 完整脱敏应使用 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(headers: Dict[str, str], api_format: APIFormat) -> Optional[str]:
|
||||
"""
|
||||
从客户端请求头提取 API Key
|
||||
|
||||
自动处理大小写,根据 API 格式使用正确的认证头和类型。
|
||||
|
||||
Args:
|
||||
headers: 原始请求头(自动处理大小写)
|
||||
api_format: API 格式
|
||||
|
||||
Returns:
|
||||
提取的 API Key,未找到返回 None
|
||||
"""
|
||||
|
||||
auth_header, auth_type = get_auth_config(api_format)
|
||||
value = get_header_value(headers, auth_header)
|
||||
if not value:
|
||||
return None
|
||||
|
||||
if auth_type == "bearer":
|
||||
# Bearer token 格式: "Bearer <token>"
|
||||
if value.lower().startswith("bearer "):
|
||||
return value[7:] # 移除 "Bearer " 前缀
|
||||
return None
|
||||
|
||||
# 直接 header 格式
|
||||
return value
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 能力需求检测
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def detect_capabilities(
|
||||
headers: Dict[str, str],
|
||||
api_format: APIFormat,
|
||||
request_body: Optional[Dict[str, Any]] = None, # noqa: ARG001 - 预留给部分格式使用
|
||||
) -> Dict[str, bool]:
|
||||
"""
|
||||
从请求头检测能力需求
|
||||
|
||||
当前支持:
|
||||
- Claude/Claude CLI: anthropic-beta 头中的 context-1m
|
||||
|
||||
Args:
|
||||
headers: 原始请求头(自动处理大小写)
|
||||
api_format: API 格式
|
||||
request_body: 请求体(部分格式可能需要)
|
||||
|
||||
Returns:
|
||||
能力需求字典,如 {"context_1m": True}
|
||||
"""
|
||||
|
||||
requirements: Dict[str, bool] = {}
|
||||
|
||||
if api_format in (APIFormat.CLAUDE, APIFormat.CLAUDE_CLI):
|
||||
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.headers 不能覆盖认证头的场景。
|
||||
"""
|
||||
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 build(self) -> Dict[str, str]:
|
||||
"""构建最终的头部字典"""
|
||||
return {original_key: value for original_key, value in self._headers.values()}
|
||||
|
||||
|
||||
def build_upstream_headers(
|
||||
original_headers: Dict[str, str],
|
||||
api_format: APIFormat,
|
||||
provider_api_key: str,
|
||||
*,
|
||||
endpoint_headers: Optional[Dict[str, str]] = None,
|
||||
extra_headers: Optional[Dict[str, str]] = None,
|
||||
drop_headers: Optional[FrozenSet[str]] = None,
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
构建发送给上游 Provider 的请求头
|
||||
|
||||
优先级(后者覆盖前者):
|
||||
1. 原始头部(排除 drop_headers)
|
||||
2. endpoint 配置头部
|
||||
3. extra_headers
|
||||
4. 认证头(最高优先级,始终设置)
|
||||
|
||||
Args:
|
||||
original_headers: 客户端原始请求头
|
||||
api_format: API 格式
|
||||
provider_api_key: Provider 的 API Key(已解密)
|
||||
endpoint_headers: Endpoint 配置的额外头部
|
||||
extra_headers: 调用方传入的额外头部
|
||||
drop_headers: 需要剔除的头部集合(None 使用默认值,空集合表示不剔除)
|
||||
|
||||
Returns:
|
||||
构建好的请求头字典
|
||||
"""
|
||||
|
||||
# 使用 is None 判断,允许显式传空集合
|
||||
if drop_headers is None:
|
||||
drop_headers = UPSTREAM_DROP_HEADERS
|
||||
|
||||
auth_header, auth_type = get_auth_config(api_format)
|
||||
auth_value = f"Bearer {provider_api_key}" if auth_type == "bearer" else provider_api_key
|
||||
|
||||
# 认证头是受保护的,不能被 endpoint_headers 覆盖
|
||||
protected_keys = {auth_header.lower(), "content-type"}
|
||||
|
||||
builder = HeaderBuilder()
|
||||
|
||||
# 1. 添加原始头部(排除 drop_headers)
|
||||
for k, v in original_headers.items():
|
||||
if k.lower() not in drop_headers:
|
||||
builder.add(k, v)
|
||||
|
||||
# 2. 添加 endpoint 头部(保护认证头)
|
||||
if endpoint_headers:
|
||||
builder.add_protected(endpoint_headers, protected_keys)
|
||||
|
||||
# 3. 添加 extra_headers
|
||||
if extra_headers:
|
||||
builder.add_many(extra_headers)
|
||||
|
||||
# 4. 设置认证头(最高优先级)
|
||||
builder.add(auth_header, auth_value)
|
||||
|
||||
# 5. 确保 Content-Type
|
||||
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: Optional[Dict[str, str]],
|
||||
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: Optional[Dict[str, str]],
|
||||
drop_headers: Optional[FrozenSet[str]] = 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: Optional[FrozenSet[str]] = 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()}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 兼容层(向后兼容,逐步废弃)
|
||||
# =============================================================================
|
||||
|
||||
# 兼容 request_builder.py 的 SENSITIVE_HEADERS
|
||||
SENSITIVE_HEADERS = UPSTREAM_DROP_HEADERS
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Adapter 统一接口
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def build_adapter_base_headers(
|
||||
api_format: APIFormat,
|
||||
api_key: str,
|
||||
*,
|
||||
include_extra: bool = True,
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
根据 API 格式构建基础请求头
|
||||
|
||||
包含:认证头 + Content-Type + 格式特定的额外头部(如 anthropic-version)
|
||||
|
||||
Args:
|
||||
api_format: API 格式
|
||||
api_key: API Key(已解密)
|
||||
include_extra: 是否包含格式特定的额外头部(默认 True)
|
||||
|
||||
Returns:
|
||||
基础请求头字典
|
||||
"""
|
||||
auth_header, auth_type = get_auth_config(api_format)
|
||||
auth_value = f"Bearer {api_key}" if auth_type == "bearer" else api_key
|
||||
|
||||
headers: Dict[str, str] = {
|
||||
auth_header: auth_value,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
if include_extra:
|
||||
extra = get_extra_headers(api_format)
|
||||
if extra:
|
||||
headers.update(extra)
|
||||
|
||||
return headers
|
||||
|
||||
|
||||
def build_adapter_headers(
|
||||
api_format: APIFormat,
|
||||
api_key: str,
|
||||
extra_headers: Optional[Dict[str, str]] = None,
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
构建完整的 Adapter 请求头
|
||||
|
||||
在基础头部上合并 extra_headers,同时保护关键头部不被覆盖。
|
||||
|
||||
Args:
|
||||
api_format: API 格式
|
||||
api_key: API Key(已解密)
|
||||
extra_headers: 调用方传入的额外头部
|
||||
|
||||
Returns:
|
||||
完整的请求头字典
|
||||
"""
|
||||
base = build_adapter_base_headers(api_format, api_key)
|
||||
|
||||
if not extra_headers:
|
||||
return base
|
||||
|
||||
protected = get_protected_keys(api_format)
|
||||
return merge_headers_with_protection(base, extra_headers, protected)
|
||||
|
||||
|
||||
def get_adapter_protected_keys(api_format: APIFormat) -> tuple[str, ...]:
|
||||
"""
|
||||
获取 Adapter 的受保护头部 key
|
||||
|
||||
用于 get_protected_header_keys() 方法返回值。
|
||||
|
||||
Args:
|
||||
api_format: API 格式
|
||||
|
||||
Returns:
|
||||
受保护的头部 key 元组
|
||||
"""
|
||||
return tuple(get_protected_keys(api_format))
|
||||
|
||||
@@ -16,6 +16,7 @@ from src.core.key_capabilities import (
|
||||
CapabilityConfigMode,
|
||||
get_user_configurable_capabilities,
|
||||
)
|
||||
from src.core.headers import get_header_value
|
||||
from src.core.logger import logger
|
||||
|
||||
# Adapter 检测器类型:接受 headers 和可选的 request_body,返回能力需求字典
|
||||
@@ -87,7 +88,7 @@ class CapabilityResolver:
|
||||
|
||||
# 3. 从请求头 X-Require-Capability 获取(显式声明)
|
||||
if request_headers:
|
||||
header_caps = request_headers.get("X-Require-Capability", "")
|
||||
header_caps = get_header_value(request_headers, "X-Require-Capability")
|
||||
if header_caps:
|
||||
for cap in header_caps.split(","):
|
||||
cap = cap.strip()
|
||||
|
||||
@@ -6,11 +6,10 @@ Provider 服务模块
|
||||
|
||||
from src.services.provider.format import normalize_api_format
|
||||
from src.services.provider.service import ProviderService
|
||||
from src.services.provider.transport import build_provider_headers, build_provider_url
|
||||
from src.services.provider.transport import build_provider_url
|
||||
|
||||
__all__ = [
|
||||
"ProviderService",
|
||||
"normalize_api_format",
|
||||
"build_provider_headers",
|
||||
"build_provider_url",
|
||||
]
|
||||
|
||||
@@ -2,75 +2,18 @@
|
||||
统一的 Provider 请求构建工具。
|
||||
|
||||
负责:
|
||||
- 根据 endpoint/key 构建标准请求头
|
||||
- 根据 API 格式或端点配置生成请求 URL
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from src.core.api_format_metadata import get_auth_config, get_default_path, resolve_api_format
|
||||
from src.core.crypto import crypto_service
|
||||
from src.core.api_format_metadata import get_default_path, resolve_api_format
|
||||
from src.core.enums import APIFormat
|
||||
from src.core.logger import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.models.database import ProviderAPIKey, ProviderEndpoint
|
||||
|
||||
|
||||
|
||||
def build_provider_headers(
|
||||
endpoint: "ProviderEndpoint",
|
||||
key: "ProviderAPIKey",
|
||||
original_headers: Optional[Dict[str, str]] = None,
|
||||
*,
|
||||
extra_headers: Optional[Dict[str, str]] = None,
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
根据 endpoint/key 构建请求头,并透传客户端自定义头。
|
||||
"""
|
||||
headers: Dict[str, str] = {}
|
||||
|
||||
# api_key 在数据库中是 NOT NULL,类型标注为 Optional 是 SQLAlchemy 限制
|
||||
decrypted_key = crypto_service.decrypt(key.api_key) # type: ignore[arg-type]
|
||||
|
||||
# 根据 API 格式自动选择认证头
|
||||
api_format = getattr(endpoint, "api_format", None)
|
||||
resolved_format = resolve_api_format(api_format)
|
||||
auth_header, auth_type = (
|
||||
get_auth_config(resolved_format) if resolved_format else ("Authorization", "bearer")
|
||||
)
|
||||
|
||||
if auth_type == "bearer":
|
||||
headers[auth_header] = f"Bearer {decrypted_key}"
|
||||
else:
|
||||
headers[auth_header] = decrypted_key
|
||||
|
||||
if endpoint.headers:
|
||||
headers.update(endpoint.headers)
|
||||
|
||||
excluded_headers = {
|
||||
"host",
|
||||
"authorization",
|
||||
"x-api-key",
|
||||
"x-goog-api-key",
|
||||
"content-length",
|
||||
"transfer-encoding",
|
||||
}
|
||||
|
||||
if original_headers:
|
||||
for name, value in original_headers.items():
|
||||
if name.lower() not in excluded_headers:
|
||||
headers[name] = value
|
||||
|
||||
if extra_headers:
|
||||
headers.update(extra_headers)
|
||||
|
||||
if "Content-Type" not in headers and "content-type" not in headers:
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
return headers
|
||||
|
||||
from src.models.database import ProviderEndpoint
|
||||
|
||||
def _normalize_base_url(base_url: str, path: str) -> str:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user