mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/ - 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层 - 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构 - 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations) - 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image - 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
1417 lines
47 KiB
Python
1417 lines
47 KiB
Python
"""
|
||
请求构建器 - 透传模式
|
||
|
||
透传模式 (Passthrough): CLI 和 Chat 等场景,原样转发请求体和头部
|
||
- 清理敏感头部:authorization, x-api-key, host, content-length 等
|
||
- 保留所有其他头部和请求体字段
|
||
- 适用于:Claude CLI、OpenAI CLI、Chat API 等场景
|
||
|
||
使用方式:
|
||
builder = PassthroughRequestBuilder()
|
||
payload, headers = builder.build(original_body, original_headers, endpoint, key)
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import copy
|
||
import re
|
||
from abc import ABC, abstractmethod
|
||
from dataclasses import dataclass
|
||
from typing import Any
|
||
|
||
from src.core.api_format import (
|
||
UPSTREAM_DROP_HEADERS,
|
||
HeaderBuilder,
|
||
get_auth_config_for_endpoint,
|
||
make_signature_key,
|
||
resolve_header_name_case,
|
||
)
|
||
from src.core.crypto import crypto_service
|
||
from src.core.logger import logger
|
||
from src.models.endpoint_models import _CONDITION_OPS, _TYPE_IS_VALUES, parse_re_flags
|
||
from src.services.provider.auth import get_provider_auth # noqa: F401
|
||
from src.services.provider.envelope import ProviderEnvelope
|
||
|
||
|
||
def _payload_item_count(value: Any) -> int | None:
|
||
"""统计顶层 prompt-bearing 容器项数量;标量按 1 处理。"""
|
||
if isinstance(value, list):
|
||
return len(value)
|
||
if value is None:
|
||
return None
|
||
if isinstance(value, (str, bytes)):
|
||
return 1 if value else 0
|
||
if isinstance(value, dict):
|
||
return 1 if value else 0
|
||
return 1
|
||
|
||
|
||
def summarize_request_payload_shape(
|
||
payload: dict[str, Any],
|
||
*,
|
||
provider_api_format: str | None,
|
||
body_rules: Any,
|
||
) -> dict[str, Any]:
|
||
"""生成最终出站 payload 的结构化摘要,避免记录正文。"""
|
||
tools = payload.get("tools")
|
||
tool_count = len(tools) if isinstance(tools, list) else None
|
||
|
||
function_declaration_count = 0
|
||
if isinstance(tools, list):
|
||
for tool in tools:
|
||
if not isinstance(tool, dict):
|
||
continue
|
||
decls = tool.get("function_declarations") or tool.get("functionDeclarations")
|
||
if isinstance(decls, list):
|
||
function_declaration_count += len(decls)
|
||
|
||
return {
|
||
"format": str(provider_api_format or "").strip().lower() or None,
|
||
"top_level_keys": sorted(payload.keys()),
|
||
"message_count": _payload_item_count(payload.get("messages")),
|
||
"input_count": _payload_item_count(payload.get("input")),
|
||
"contents_count": _payload_item_count(payload.get("contents")),
|
||
"tool_count": tool_count,
|
||
"function_declaration_count": function_declaration_count,
|
||
"has_system": any(
|
||
k in payload for k in ("system", "system_instruction", "systemInstruction")
|
||
),
|
||
"has_instructions": "instructions" in payload,
|
||
"has_tool_choice": any(
|
||
k in payload for k in ("tool_choice", "toolChoice", "tool_config", "toolConfig")
|
||
),
|
||
"has_generation_config": any(
|
||
k in payload for k in ("generation_config", "generationConfig")
|
||
),
|
||
"has_prompt_cache_key": bool(str(payload.get("prompt_cache_key") or "").strip()),
|
||
"body_rule_count": len(body_rules) if isinstance(body_rules, list) else 0,
|
||
}
|
||
|
||
|
||
# ==============================================================================
|
||
# 统一的头部配置常量
|
||
# ==============================================================================
|
||
|
||
# 兼容别名:历史代码使用 SENSITIVE_HEADERS 命名
|
||
SENSITIVE_HEADERS: frozenset[str] = UPSTREAM_DROP_HEADERS
|
||
|
||
# ==============================================================================
|
||
# 测试请求常量与辅助函数
|
||
# ==============================================================================
|
||
|
||
# 标准测试请求体(OpenAI 格式)
|
||
# 用于 check_endpoint 等测试场景,使用简单安全的消息内容避免触发安全过滤
|
||
DEFAULT_TEST_REQUEST: dict[str, Any] = {
|
||
"messages": [{"role": "user", "content": "Hi"}],
|
||
"max_tokens": 5,
|
||
"temperature": 0,
|
||
}
|
||
|
||
|
||
def get_test_request_data(request_data: dict[str, Any] | None = None) -> dict[str, Any]:
|
||
"""获取测试请求数据
|
||
|
||
如果传入 request_data,则合并到默认测试请求中;
|
||
否则使用默认测试请求。
|
||
|
||
Args:
|
||
request_data: 用户提供的请求数据(会覆盖默认值)
|
||
|
||
Returns:
|
||
合并后的测试请求数据(OpenAI 格式)
|
||
"""
|
||
if request_data:
|
||
merged = DEFAULT_TEST_REQUEST.copy()
|
||
merged.update(request_data)
|
||
return merged
|
||
return DEFAULT_TEST_REQUEST.copy()
|
||
|
||
|
||
def build_test_request_body(
|
||
format_id: str,
|
||
request_data: dict[str, Any] | None = None,
|
||
*,
|
||
target_variant: str | None = None,
|
||
) -> dict[str, Any]:
|
||
"""构建测试请求体,自动处理格式转换
|
||
|
||
使用格式转换注册表将 OpenAI 格式的测试请求转换为目标格式。
|
||
|
||
Args:
|
||
format_id: 目标 endpoint signature(如 "claude:chat", "gemini:chat", "openai:cli")
|
||
request_data: 可选的请求数据,会与默认测试请求合并
|
||
target_variant: 目标变体(如 "codex"),用于同格式但有细微差异的上游
|
||
|
||
Returns:
|
||
转换为目标 API 格式的请求体
|
||
"""
|
||
from src.core.api_format.conversion import (
|
||
format_conversion_registry,
|
||
register_default_normalizers,
|
||
)
|
||
|
||
register_default_normalizers()
|
||
|
||
# 获取测试请求数据(OpenAI 格式)
|
||
source_data = get_test_request_data(request_data)
|
||
|
||
# 直接使用目标格式进行转换,不再转换为基础格式
|
||
# 这样 openai:cli 会正确转换为 Responses API 格式
|
||
return format_conversion_registry.convert_request(
|
||
source_data,
|
||
make_signature_key("openai", "chat"),
|
||
format_id,
|
||
target_variant=target_variant,
|
||
)
|
||
|
||
|
||
# ==============================================================================
|
||
# 请求体规则应用
|
||
# ==============================================================================
|
||
|
||
|
||
@dataclass(frozen=True, slots=True)
|
||
class _WildcardSlice:
|
||
"""表示数组通配符路径段: [*] 或 [start-end]"""
|
||
|
||
start: int | None # None 表示 [*]
|
||
end: int | None
|
||
|
||
def resolve(self, length: int) -> range:
|
||
"""根据实际数组长度返回索引 range"""
|
||
if self.start is None:
|
||
return range(length)
|
||
s = max(0, self.start)
|
||
e = min(length - 1, self.end if self.end is not None else length - 1)
|
||
return range(s, e + 1) if s <= e else range(0)
|
||
|
||
|
||
# 路径段类型:str 表示 dict key,int 表示数组索引,_WildcardSlice 表示通配
|
||
PathSegment = str | int | _WildcardSlice
|
||
|
||
_RANGE_RE = re.compile(r"^(\d+)\s*-\s*(\d+)$")
|
||
|
||
|
||
def _parse_path(path: str) -> list[PathSegment]:
|
||
"""
|
||
解析路径,支持点号分隔、转义、数组索引、通配符和范围。
|
||
|
||
Examples:
|
||
"metadata.user.name" -> ["metadata", "user", "name"]
|
||
"config\\.v1.enabled" -> ["config.v1", "enabled"]
|
||
"messages[0].content" -> ["messages", 0, "content"]
|
||
"data[0].items[2].name" -> ["data", 0, "items", 2, "name"]
|
||
"messages[-1]" -> ["messages", -1]
|
||
"matrix[0][1]" -> ["matrix", 0, 1]
|
||
"tools[*].name" -> ["tools", _WildcardSlice(None, None), "name"]
|
||
"tools[0-4].name" -> ["tools", _WildcardSlice(0, 4), "name"]
|
||
|
||
约束:
|
||
- 不允许空段(例如:".a" / "a." / "a..b"),遇到则返回空列表表示无效路径。
|
||
- 仅对 "\\." 做特殊处理;其他反斜杠组合按字面量保留。
|
||
- 数组索引必须是整数(支持负数索引)。
|
||
- [*] 表示遍历数组所有元素。
|
||
- [N-M] 表示遍历数组索引 N 到 M(含两端)。
|
||
"""
|
||
raw = (path or "").strip()
|
||
if not raw:
|
||
return []
|
||
|
||
parts: list[PathSegment] = []
|
||
current: list[str] = []
|
||
expect_key = True # 是否期望下一个片段是 dict key
|
||
|
||
i = 0
|
||
while i < len(raw):
|
||
ch = raw[i]
|
||
|
||
# 转义点号:\\.
|
||
if ch == "\\" and i + 1 < len(raw) and raw[i + 1] == ".":
|
||
current.append(".")
|
||
expect_key = False
|
||
i += 2
|
||
continue
|
||
|
||
# 点号分隔符
|
||
if ch == ".":
|
||
if current:
|
||
parts.append("".join(current))
|
||
current = []
|
||
elif expect_key:
|
||
# 空段(如 ".a" 或 "a..b")
|
||
return []
|
||
expect_key = True
|
||
i += 1
|
||
continue
|
||
|
||
# 数组索引:[N] / [*] / [N-M]
|
||
if ch == "[":
|
||
# 先将当前累积的 key 入栈
|
||
if current:
|
||
parts.append("".join(current))
|
||
current = []
|
||
|
||
# 查找闭合括号
|
||
j = i + 1
|
||
while j < len(raw) and raw[j] != "]":
|
||
j += 1
|
||
if j >= len(raw):
|
||
return [] # 未闭合的括号
|
||
|
||
index_str = raw[i + 1 : j].strip()
|
||
if not index_str:
|
||
return [] # 空索引
|
||
|
||
# [*] 通配符
|
||
if index_str == "*":
|
||
parts.append(_WildcardSlice(None, None))
|
||
else:
|
||
# [N-M] 范围
|
||
m = _RANGE_RE.match(index_str)
|
||
if m:
|
||
parts.append(_WildcardSlice(int(m.group(1)), int(m.group(2))))
|
||
else:
|
||
# 普通整数索引
|
||
try:
|
||
idx = int(index_str)
|
||
except ValueError:
|
||
return [] # 非整数索引
|
||
parts.append(idx)
|
||
|
||
expect_key = False
|
||
i = j + 1
|
||
continue
|
||
|
||
current.append(ch)
|
||
expect_key = False
|
||
i += 1
|
||
|
||
# 收尾:将剩余的 key 入栈
|
||
if current:
|
||
parts.append("".join(current))
|
||
elif expect_key:
|
||
# 尾部悬挂的点号(如 "a.")
|
||
return []
|
||
|
||
return parts if parts else []
|
||
|
||
|
||
def _has_wildcard(parts: list[PathSegment]) -> bool:
|
||
"""检查路径段列表中是否包含通配符"""
|
||
return any(isinstance(p, _WildcardSlice) for p in parts)
|
||
|
||
|
||
def _expand_wildcard_paths(
|
||
obj: Any, parts: list[PathSegment], *, require_leaf: bool = False
|
||
) -> list[list[str | int]]:
|
||
"""
|
||
将含通配符的路径段展开为具体的路径段列表。
|
||
|
||
遍历 obj 结构,遇到 _WildcardSlice 时根据实际数组长度展开为具体索引。
|
||
返回的每条路径都是纯 str|int 段,不含通配符。
|
||
|
||
Args:
|
||
obj: 要遍历的数据结构
|
||
parts: 含通配符的路径段列表
|
||
require_leaf: 是否要求叶子节点存在(False 时只要父级存在即可,适用于 set)
|
||
"""
|
||
result: list[list[str | int]] = []
|
||
|
||
def _recurse(current: Any, idx: int, prefix: list[str | int]) -> None:
|
||
if idx == len(parts):
|
||
result.append(prefix[:])
|
||
return
|
||
|
||
seg = parts[idx]
|
||
is_last = idx == len(parts) - 1
|
||
|
||
if isinstance(seg, _WildcardSlice):
|
||
if not isinstance(current, list):
|
||
return
|
||
for i in seg.resolve(len(current)):
|
||
prefix.append(i)
|
||
try:
|
||
_recurse(current[i], idx + 1, prefix)
|
||
except IndexError:
|
||
pass
|
||
prefix.pop()
|
||
elif isinstance(seg, int):
|
||
if isinstance(current, list):
|
||
try:
|
||
prefix.append(seg)
|
||
_recurse(current[seg], idx + 1, prefix)
|
||
prefix.pop()
|
||
except IndexError:
|
||
prefix.pop()
|
||
else:
|
||
# str key
|
||
if isinstance(current, dict):
|
||
if seg in current:
|
||
prefix.append(seg)
|
||
_recurse(current[seg], idx + 1, prefix)
|
||
prefix.pop()
|
||
elif is_last and not require_leaf:
|
||
# 叶子节点不存在但允许创建(set 场景)
|
||
prefix.append(seg)
|
||
result.append(prefix[:])
|
||
prefix.pop()
|
||
|
||
_recurse(obj, 0, [])
|
||
return result
|
||
|
||
|
||
def _segments_to_path(segments: list[str | int]) -> str:
|
||
"""将路径段列表转回路径字符串(用于调用现有的 _set/_get/_delete 函数)"""
|
||
parts: list[str] = []
|
||
for seg in segments:
|
||
if isinstance(seg, int):
|
||
parts.append(f"[{seg}]")
|
||
else:
|
||
# 转义字面量点号
|
||
escaped = seg.replace(".", "\\.")
|
||
if parts and not parts[-1].endswith("]"):
|
||
parts.append(f".{escaped}")
|
||
else:
|
||
parts.append(escaped)
|
||
return "".join(parts)
|
||
|
||
|
||
def _get_nested_value(obj: Any, path: str) -> tuple[bool, Any]:
|
||
"""
|
||
获取嵌套值,支持 dict 和 list 混合遍历
|
||
|
||
Returns:
|
||
(found, value) - found 为 True 时 value 有效
|
||
"""
|
||
parts = _parse_path(path)
|
||
if not parts:
|
||
return False, None
|
||
|
||
current: Any = obj
|
||
for segment in parts:
|
||
if isinstance(segment, int):
|
||
if isinstance(current, list):
|
||
try:
|
||
current = current[segment]
|
||
except IndexError:
|
||
return False, None
|
||
else:
|
||
return False, None
|
||
else:
|
||
if isinstance(current, dict) and segment in current:
|
||
current = current[segment]
|
||
else:
|
||
return False, None
|
||
return True, current
|
||
|
||
|
||
def _set_nested_value(obj: dict[str, Any], path: str, value: Any) -> bool:
|
||
"""
|
||
设置嵌套值,支持 dict 和 list 混合遍历。
|
||
|
||
- dict 中间层:下一段为 str key 时自动创建(覆写语义);下一段为 int 时要求已存在 list。
|
||
- list 中间层:必须已存在且索引有效。
|
||
- list 元素赋值:要求索引在范围内。
|
||
|
||
Returns:
|
||
True: 写入成功
|
||
False: 路径无效或结构不匹配
|
||
"""
|
||
parts = _parse_path(path)
|
||
if not parts:
|
||
return False
|
||
|
||
current: Any = obj
|
||
for i in range(len(parts) - 1):
|
||
segment = parts[i]
|
||
next_segment = parts[i + 1]
|
||
|
||
if isinstance(segment, int):
|
||
# 遍历数组元素
|
||
if not isinstance(current, list):
|
||
return False
|
||
try:
|
||
current = current[segment]
|
||
except IndexError:
|
||
return False
|
||
else:
|
||
# 遍历 dict key
|
||
if not isinstance(current, dict):
|
||
return False
|
||
child = current.get(segment)
|
||
|
||
if isinstance(next_segment, int):
|
||
# 下一段是数组索引 → child 必须已经是 list
|
||
if not isinstance(child, list):
|
||
return False
|
||
current = child
|
||
else:
|
||
# 下一段是 dict key → 自动创建 dict(覆写语义)
|
||
if not isinstance(child, dict):
|
||
child = {}
|
||
current[segment] = child
|
||
current = child
|
||
|
||
# 写入最终值
|
||
last = parts[-1]
|
||
if isinstance(last, int):
|
||
if not isinstance(current, list):
|
||
return False
|
||
try:
|
||
current[last] = value
|
||
return True
|
||
except IndexError:
|
||
return False
|
||
else:
|
||
if not isinstance(current, dict):
|
||
return False
|
||
current[last] = value
|
||
return True
|
||
|
||
|
||
def _delete_nested_value(obj: dict[str, Any], path: str) -> bool:
|
||
"""
|
||
删除嵌套值,支持 dict 和 list 混合遍历
|
||
|
||
对于 list 元素,使用 del 删除(会移动后续元素的索引)。
|
||
|
||
Returns:
|
||
True: 删除成功
|
||
False: 路径不存在或无效
|
||
"""
|
||
parts = _parse_path(path)
|
||
if not parts:
|
||
return False
|
||
|
||
current: Any = obj
|
||
for segment in parts[:-1]:
|
||
if isinstance(segment, int):
|
||
if isinstance(current, list):
|
||
try:
|
||
current = current[segment]
|
||
except IndexError:
|
||
return False
|
||
else:
|
||
return False
|
||
else:
|
||
if isinstance(current, dict) and segment in current:
|
||
current = current[segment]
|
||
else:
|
||
return False
|
||
|
||
last = parts[-1]
|
||
if isinstance(last, int):
|
||
if isinstance(current, list):
|
||
try:
|
||
del current[last]
|
||
return True
|
||
except IndexError:
|
||
return False
|
||
return False
|
||
else:
|
||
if isinstance(current, dict) and last in current:
|
||
del current[last]
|
||
return True
|
||
return False
|
||
|
||
|
||
def _rename_nested_value(obj: dict[str, Any], from_path: str, to_path: str) -> bool:
|
||
"""
|
||
重命名嵌套值(移动到新路径),支持 dict 和 list 混合遍历
|
||
|
||
Returns:
|
||
True: 重命名成功
|
||
False: 源路径不存在或路径无效
|
||
"""
|
||
src = (from_path or "").strip()
|
||
dst = (to_path or "").strip()
|
||
if not src or not dst:
|
||
return False
|
||
if src == dst:
|
||
found, _ = _get_nested_value(obj, src)
|
||
return found
|
||
|
||
found, value = _get_nested_value(obj, src)
|
||
if not found:
|
||
return False
|
||
|
||
# 先 set 再 delete,避免 set 失败时源值已被删除导致数据丢失
|
||
if not _set_nested_value(obj, dst, value):
|
||
return False
|
||
_delete_nested_value(obj, src)
|
||
return True
|
||
|
||
|
||
def _extract_path(
|
||
rule: dict[str, Any],
|
||
key: str = "path",
|
||
) -> str | None:
|
||
"""从规则中提取并校验 path 字段,返回 strip 后的路径或 None。"""
|
||
raw = rule.get(key, "")
|
||
if not isinstance(raw, str):
|
||
return None
|
||
path = raw.strip()
|
||
parts = _parse_path(path)
|
||
if not parts:
|
||
return None
|
||
return path
|
||
|
||
|
||
_ORIGINAL_PLACEHOLDER = "{{$original}}"
|
||
|
||
# ==============================================================================
|
||
# 命名风格转换
|
||
# ==============================================================================
|
||
|
||
_NAME_STYLE_VALUES = frozenset(
|
||
{"snake_case", "camelCase", "PascalCase", "kebab-case", "capitalize"}
|
||
)
|
||
|
||
# 拆分标识符为单词列表(支持 camelCase / PascalCase / snake_case / kebab-case / 混合)
|
||
_WORD_SPLIT_RE = re.compile(r"[A-Z]?[a-z]+|[A-Z]+(?=[A-Z][a-z]|\d|\b)|[A-Z]|[0-9]+")
|
||
|
||
|
||
def _split_identifier(name: str) -> list[str]:
|
||
"""将标识符拆分为小写单词列表"""
|
||
# 先把常见分隔符替换为空格
|
||
normalized = name.replace("_", " ").replace("-", " ")
|
||
words = _WORD_SPLIT_RE.findall(normalized)
|
||
return [w.lower() for w in words if w]
|
||
|
||
|
||
def _convert_name_style(name: str, style: str) -> str:
|
||
"""将标识符转换为指定命名风格"""
|
||
words = _split_identifier(name)
|
||
if not words:
|
||
return name
|
||
|
||
if style == "snake_case":
|
||
return "_".join(words)
|
||
elif style == "camelCase":
|
||
return words[0] + "".join(w.capitalize() for w in words[1:])
|
||
elif style == "PascalCase":
|
||
return "".join(w.capitalize() for w in words)
|
||
elif style == "kebab-case":
|
||
return "-".join(words)
|
||
elif style == "capitalize":
|
||
# 仅首字母大写,保留其余部分不变
|
||
return name[0].upper() + name[1:] if name else name
|
||
return name
|
||
|
||
|
||
def _contains_original_placeholder(value: Any) -> bool:
|
||
"""递归检查 value 中是否包含 {{$original}} 占位符"""
|
||
if isinstance(value, str):
|
||
return _ORIGINAL_PLACEHOLDER in value
|
||
if isinstance(value, dict):
|
||
return any(_contains_original_placeholder(v) for v in value.values())
|
||
if isinstance(value, list):
|
||
return any(_contains_original_placeholder(item) for item in value)
|
||
return False
|
||
|
||
|
||
def _resolve_original_placeholder(template: Any, original: Any) -> Any:
|
||
"""递归解析模板中的 {{$original}} 占位符。
|
||
|
||
- 字符串完全匹配 {{$original}} → 直接返回原值(保留原始类型)
|
||
- 字符串部分包含 {{$original}} → str(original) 插值
|
||
- dict → 递归处理每个 value
|
||
- list → 递归处理每个元素
|
||
- 其他 → 原样返回
|
||
"""
|
||
if isinstance(template, str):
|
||
if template == _ORIGINAL_PLACEHOLDER:
|
||
return original
|
||
if _ORIGINAL_PLACEHOLDER in template:
|
||
return template.replace(_ORIGINAL_PLACEHOLDER, str(original))
|
||
return template
|
||
if isinstance(template, dict):
|
||
return {k: _resolve_original_placeholder(v, original) for k, v in template.items()}
|
||
if isinstance(template, list):
|
||
return [_resolve_original_placeholder(item, original) for item in template]
|
||
return template
|
||
|
||
|
||
_ITEM_PREFIX = "$item."
|
||
_ITEM_EXACT = "$item"
|
||
|
||
|
||
def _get_condition_children(
|
||
condition: dict[str, Any],
|
||
) -> tuple[str, list[Any]] | None:
|
||
"""如果 condition 是 all/any 组合节点,返回 (key, children);否则返回 None。"""
|
||
for key in ("all", "any"):
|
||
children = condition.get(key)
|
||
if isinstance(children, list):
|
||
return key, children
|
||
return None
|
||
|
||
|
||
def _has_item_ref(condition: dict[str, Any] | None) -> bool:
|
||
"""检查 condition 的 path 是否包含 $item 引用"""
|
||
if not condition or not isinstance(condition, dict):
|
||
return False
|
||
group = _get_condition_children(condition)
|
||
if group is not None:
|
||
return any(_has_item_ref(c) for c in group[1] if isinstance(c, dict))
|
||
path = condition.get("path", "")
|
||
return isinstance(path, str) and (
|
||
path.strip().startswith(_ITEM_PREFIX) or path.strip() == _ITEM_EXACT
|
||
)
|
||
|
||
|
||
def _resolve_item_condition(
|
||
condition: dict[str, Any],
|
||
item_path_prefix: str,
|
||
) -> dict[str, Any]:
|
||
"""将 condition 中的 $item 引用替换为具体的元素路径前缀。
|
||
|
||
例如:
|
||
condition = {"path": "$item.name", "op": "in", "value": ["writer"]}
|
||
item_path_prefix = "tools[0]"
|
||
-> {"path": "tools[0].name", "op": "in", "value": ["writer"]}
|
||
|
||
condition = {"path": "$item", "op": "type_is", "value": "object"}
|
||
item_path_prefix = "tools[0]"
|
||
-> {"path": "tools[0]", "op": "type_is", "value": "object"}
|
||
"""
|
||
group = _get_condition_children(condition)
|
||
if group is not None:
|
||
key, children = group
|
||
return {
|
||
key: [
|
||
_resolve_item_condition(c, item_path_prefix) if isinstance(c, dict) else c
|
||
for c in children
|
||
]
|
||
}
|
||
|
||
resolved = dict(condition)
|
||
raw_path = resolved.get("path", "").strip()
|
||
if raw_path == _ITEM_EXACT:
|
||
resolved["path"] = item_path_prefix
|
||
elif raw_path.startswith(_ITEM_PREFIX):
|
||
suffix = raw_path[len(_ITEM_PREFIX) :]
|
||
resolved["path"] = f"{item_path_prefix}.{suffix}"
|
||
return resolved
|
||
|
||
|
||
def _get_item_prefix_from_concrete(
|
||
concrete_segs: list[str | int],
|
||
wildcard_parts: list[PathSegment],
|
||
) -> str:
|
||
"""从展开后的具体路径段中,提取通配符所在层级的元素路径前缀。
|
||
|
||
例如:
|
||
concrete_segs = ["tools", 0, "name"]
|
||
wildcard_parts = ["tools", _WildcardSlice, "name"]
|
||
-> "tools[0]" (通配符在 index 1,取 concrete_segs[:2])
|
||
|
||
concrete_segs = ["data", 1, "items", 2, "name"]
|
||
wildcard_parts = ["data", _WildcardSlice, "items", _WildcardSlice, "name"]
|
||
-> "data[1].items[2]" (取到最后一个通配符位置+1)
|
||
"""
|
||
# 找到最后一个通配符在 wildcard_parts 中的位置
|
||
last_wc_idx = 0
|
||
for i, seg in enumerate(wildcard_parts):
|
||
if isinstance(seg, _WildcardSlice):
|
||
last_wc_idx = i
|
||
|
||
# concrete_segs 中对应位置 +1 就是元素前缀的结束
|
||
prefix_segs = concrete_segs[: last_wc_idx + 1]
|
||
return _segments_to_path(prefix_segs)
|
||
|
||
|
||
def _iter_wildcard_targets(
|
||
result: dict[str, Any],
|
||
path: str,
|
||
parts: list[PathSegment],
|
||
condition: dict[str, Any] | None,
|
||
item_condition: bool,
|
||
*,
|
||
original_body: dict[str, Any] | None = None,
|
||
require_leaf: bool = False,
|
||
reverse: bool = False,
|
||
) -> list[str]:
|
||
"""通配符路径展开 + $item 条件过滤的通用逻辑。
|
||
|
||
如果路径不含通配符,返回 [path] 本身(单元素列表)。
|
||
如果含通配符,展开后逐条评估 $item 条件,返回通过条件的具体路径列表。
|
||
|
||
Args:
|
||
result: 当前请求体(用于展开和条件评估)
|
||
path: 原始路径字符串(不含通配符时直接返回)
|
||
parts: 已解析的路径段列表
|
||
condition: 规则的 condition 字典
|
||
item_condition: condition 是否包含 $item 引用
|
||
require_leaf: 是否要求叶子节点存在
|
||
reverse: 是否倒序返回(drop 场景需要倒序避免索引偏移)
|
||
"""
|
||
if not _has_wildcard(parts):
|
||
return [path]
|
||
|
||
expanded = _expand_wildcard_paths(result, parts, require_leaf=require_leaf)
|
||
if reverse:
|
||
expanded = list(reversed(expanded))
|
||
|
||
targets: list[str] = []
|
||
for concrete_segs in expanded:
|
||
if item_condition:
|
||
prefix = _get_item_prefix_from_concrete(concrete_segs, parts)
|
||
resolved = _resolve_item_condition(condition, prefix) # type: ignore[arg-type]
|
||
if not evaluate_condition(result, resolved, original_body=original_body):
|
||
continue
|
||
targets.append(_segments_to_path(concrete_segs))
|
||
return targets
|
||
|
||
|
||
# ==============================================================================
|
||
# 条件评估器
|
||
# ==============================================================================
|
||
|
||
# _CONDITION_OPS / _TYPE_IS_VALUES 从 endpoint_models 导入,避免重复定义
|
||
|
||
_SIMPLE_TYPE_MAP: dict[str, type] = {
|
||
"string": str,
|
||
"array": list,
|
||
"object": dict,
|
||
}
|
||
|
||
|
||
def evaluate_condition(
|
||
body: dict[str, Any],
|
||
condition: dict[str, Any],
|
||
original_body: dict[str, Any] | None = None,
|
||
) -> bool:
|
||
"""
|
||
评估单个条件表达式,决定规则是否应该执行。
|
||
|
||
条件格式: {"path": "model", "op": "starts_with", "value": "claude"}
|
||
|
||
条件无效时返回 False(跳过该规则,fail-closed)。
|
||
"""
|
||
if not isinstance(condition, dict):
|
||
return False
|
||
|
||
if "all" in condition:
|
||
children = condition.get("all")
|
||
if not isinstance(children, list) or not children:
|
||
return False
|
||
return all(
|
||
isinstance(child, dict) and evaluate_condition(body, child, original_body=original_body)
|
||
for child in children
|
||
)
|
||
|
||
if "any" in condition:
|
||
children = condition.get("any")
|
||
if not isinstance(children, list) or not children:
|
||
return False
|
||
return any(
|
||
isinstance(child, dict) and evaluate_condition(body, child, original_body=original_body)
|
||
for child in children
|
||
)
|
||
|
||
op = condition.get("op")
|
||
if not isinstance(op, str) or op not in _CONDITION_OPS:
|
||
return False
|
||
|
||
path = condition.get("path")
|
||
if not isinstance(path, str) or not path.strip():
|
||
return False
|
||
|
||
source = condition.get("source", "current")
|
||
if not isinstance(source, str) or source not in {"current", "original"}:
|
||
return False
|
||
target = original_body if source == "original" and original_body is not None else body
|
||
|
||
found, current_val = _get_nested_value(target, path.strip())
|
||
|
||
# 存在性检查:不需要 value
|
||
if op == "exists":
|
||
return found
|
||
if op == "not_exists":
|
||
return not found
|
||
|
||
# 其他操作符要求字段存在
|
||
if not found:
|
||
return False
|
||
|
||
expected = condition.get("value")
|
||
|
||
# 相等/不等
|
||
if op == "eq":
|
||
return current_val == expected
|
||
if op == "neq":
|
||
return current_val != expected
|
||
|
||
# 数值比较
|
||
if op in ("gt", "lt", "gte", "lte"):
|
||
if not isinstance(current_val, (int, float)) or not isinstance(expected, (int, float)):
|
||
return False
|
||
if op == "gt":
|
||
return current_val > expected
|
||
if op == "lt":
|
||
return current_val < expected
|
||
if op == "gte":
|
||
return current_val >= expected
|
||
return current_val <= expected # lte
|
||
|
||
# 字符串操作
|
||
if op == "starts_with":
|
||
return (
|
||
isinstance(current_val, str)
|
||
and isinstance(expected, str)
|
||
and current_val.startswith(expected)
|
||
)
|
||
if op == "ends_with":
|
||
return (
|
||
isinstance(current_val, str)
|
||
and isinstance(expected, str)
|
||
and current_val.endswith(expected)
|
||
)
|
||
if op == "contains":
|
||
if isinstance(current_val, str) and isinstance(expected, str):
|
||
return expected in current_val
|
||
if isinstance(current_val, list):
|
||
return expected in current_val
|
||
return False
|
||
if op == "matches":
|
||
if not isinstance(current_val, str) or not isinstance(expected, str):
|
||
return False
|
||
try:
|
||
return re.search(expected, current_val) is not None
|
||
except re.error:
|
||
return False
|
||
|
||
# 列表包含
|
||
if op == "in":
|
||
return isinstance(expected, list) and current_val in expected
|
||
|
||
# 类型判断
|
||
if op == "type_is":
|
||
if not isinstance(expected, str) or expected not in _TYPE_IS_VALUES:
|
||
return False
|
||
# bool 是 int 的子类,需要特殊处理
|
||
if expected == "number":
|
||
return isinstance(current_val, (int, float)) and not isinstance(current_val, bool)
|
||
if expected == "boolean":
|
||
return isinstance(current_val, bool)
|
||
if expected == "null":
|
||
return current_val is None
|
||
return isinstance(current_val, _SIMPLE_TYPE_MAP[expected])
|
||
|
||
return False
|
||
|
||
|
||
def apply_body_rules(
|
||
body: dict[str, Any],
|
||
rules: list[dict[str, Any]],
|
||
original_body: dict[str, Any] | None = None,
|
||
) -> dict[str, Any]:
|
||
"""
|
||
应用请求体规则
|
||
|
||
路径语法:
|
||
- 使用点号分隔层级:metadata.user.name
|
||
- 转义字面量点号:config\\.v1.enabled -> key "config.v1" 下的 "enabled"
|
||
- 使用方括号访问数组元素:messages[0].content
|
||
- 支持多层嵌套:data[0].items[2].name
|
||
- 支持负数索引:messages[-1]
|
||
- 支持连续数组索引:matrix[0][1]
|
||
- 通配符 [*]:遍历数组所有元素,如 tools[*].name
|
||
- 范围 [N-M]:遍历数组索引 N 到 M(含两端),如 tools[0-4].name
|
||
|
||
支持的规则类型:
|
||
- set: 设置/覆盖字段 {"action": "set", "path": "metadata.user_id", "value": 123}
|
||
value 中的字符串 {{$original}} 会被替换为该路径的原值(完全匹配时保留类型)
|
||
- drop: 删除字段 {"action": "drop", "path": "unwanted_field"}
|
||
- rename: 重命名字段 {"action": "rename", "from": "old.key", "to": "new.key"}
|
||
- append: 向数组追加元素 {"action": "append", "path": "messages", "value": {...}}
|
||
- insert: 在数组指定位置插入元素 {"action": "insert", "path": "messages", "index": 0, "value": {...}}
|
||
- regex_replace: 正则替换字符串值 {"action": "regex_replace", "path": "messages[0].content",
|
||
"pattern": "\\bfoo\\b", "replacement": "bar", "flags": "i", "count": 0}
|
||
- name_style: 转换字符串命名风格 {"action": "name_style", "path": "tools[*].name",
|
||
"style": "camelCase"}
|
||
支持的风格: snake_case, camelCase, PascalCase, kebab-case, capitalize
|
||
|
||
Args:
|
||
body: 原始请求体
|
||
rules: 规则列表
|
||
original_body: 条件评估使用的原始请求体;未提供时回退到当前 body
|
||
|
||
Returns:
|
||
应用规则后的请求体
|
||
"""
|
||
if not rules:
|
||
return body
|
||
|
||
# 深拷贝,避免修改原始数据(尤其是嵌套 dict/list)
|
||
result = copy.deepcopy(body)
|
||
|
||
for rule in rules:
|
||
if not isinstance(rule, dict):
|
||
continue
|
||
|
||
# 条件触发:
|
||
# - $item 引用的 condition 延迟到通配符循环内逐元素评估
|
||
# - 普通 condition 在此全局评估,不满足则跳过整条规则
|
||
condition = rule.get("condition")
|
||
item_condition = _has_item_ref(condition)
|
||
if condition is not None and not item_condition:
|
||
if not evaluate_condition(result, condition, original_body=original_body):
|
||
continue
|
||
|
||
action = rule.get("action")
|
||
if not isinstance(action, str):
|
||
continue
|
||
action = action.strip().lower()
|
||
|
||
if action == "set":
|
||
path = _extract_path(rule)
|
||
if not path:
|
||
continue
|
||
parts = _parse_path(path)
|
||
for target_path in _iter_wildcard_targets(
|
||
result, path, parts, condition, item_condition, original_body=original_body
|
||
):
|
||
value = rule.get("value")
|
||
if _contains_original_placeholder(value):
|
||
found, original = _get_nested_value(result, target_path)
|
||
value = _resolve_original_placeholder(value, original if found else None)
|
||
_set_nested_value(result, target_path, value)
|
||
|
||
elif action == "drop":
|
||
path = _extract_path(rule)
|
||
if not path:
|
||
continue
|
||
parts = _parse_path(path)
|
||
for target_path in _iter_wildcard_targets(
|
||
result,
|
||
path,
|
||
parts,
|
||
condition,
|
||
item_condition,
|
||
original_body=original_body,
|
||
require_leaf=True,
|
||
reverse=True,
|
||
):
|
||
_delete_nested_value(result, target_path)
|
||
|
||
elif action == "rename":
|
||
raw_from = rule.get("from", "")
|
||
raw_to = rule.get("to", "")
|
||
if not isinstance(raw_from, str) or not isinstance(raw_to, str):
|
||
continue
|
||
from_path = raw_from.strip()
|
||
to_path = raw_to.strip()
|
||
if not from_path or not to_path:
|
||
continue
|
||
from_parts = _parse_path(from_path)
|
||
to_parts = _parse_path(to_path)
|
||
if not from_parts or not to_parts:
|
||
continue
|
||
|
||
# rename 不支持通配符(语义不明确)
|
||
if _has_wildcard(from_parts) or _has_wildcard(to_parts):
|
||
continue
|
||
|
||
_rename_nested_value(result, from_path, to_path)
|
||
|
||
elif action == "append":
|
||
path = _extract_path(rule)
|
||
if not path:
|
||
continue
|
||
parts = _parse_path(path)
|
||
for target_path in _iter_wildcard_targets(
|
||
result,
|
||
path,
|
||
parts,
|
||
condition,
|
||
item_condition,
|
||
original_body=original_body,
|
||
require_leaf=True,
|
||
):
|
||
found, target = _get_nested_value(result, target_path)
|
||
if found and isinstance(target, list):
|
||
target.append(rule.get("value"))
|
||
|
||
elif action == "insert":
|
||
path = _extract_path(rule)
|
||
if not path:
|
||
continue
|
||
index = rule.get("index")
|
||
if not isinstance(index, int):
|
||
continue
|
||
# insert 不支持通配符(索引语义冲突)
|
||
found, target = _get_nested_value(result, path)
|
||
if not found or not isinstance(target, list):
|
||
continue
|
||
target.insert(index, rule.get("value"))
|
||
|
||
elif action == "regex_replace":
|
||
path = _extract_path(rule)
|
||
if not path:
|
||
continue
|
||
pattern = rule.get("pattern")
|
||
replacement = rule.get("replacement", "")
|
||
if not isinstance(pattern, str) or not isinstance(replacement, str):
|
||
continue
|
||
if not pattern:
|
||
continue
|
||
|
||
flags_raw = rule.get("flags", "")
|
||
re_flags = parse_re_flags(flags_raw if isinstance(flags_raw, str) else "")
|
||
|
||
count = rule.get("count", 0)
|
||
if not isinstance(count, int) or count < 0:
|
||
count = 0
|
||
|
||
try:
|
||
compiled = re.compile(pattern, re_flags)
|
||
except re.error:
|
||
continue
|
||
|
||
parts = _parse_path(path)
|
||
for target_path in _iter_wildcard_targets(
|
||
result,
|
||
path,
|
||
parts,
|
||
condition,
|
||
item_condition,
|
||
original_body=original_body,
|
||
require_leaf=True,
|
||
):
|
||
found, current_val = _get_nested_value(result, target_path)
|
||
if found and isinstance(current_val, str):
|
||
new_val = compiled.sub(replacement, current_val, count=count)
|
||
_set_nested_value(result, target_path, new_val)
|
||
|
||
elif action == "name_style":
|
||
path = _extract_path(rule)
|
||
if not path:
|
||
continue
|
||
style = rule.get("style")
|
||
if not isinstance(style, str) or style not in _NAME_STYLE_VALUES:
|
||
continue
|
||
parts = _parse_path(path)
|
||
for target_path in _iter_wildcard_targets(
|
||
result,
|
||
path,
|
||
parts,
|
||
condition,
|
||
item_condition,
|
||
original_body=original_body,
|
||
require_leaf=True,
|
||
):
|
||
found, current_val = _get_nested_value(result, target_path)
|
||
if found and isinstance(current_val, str):
|
||
_set_nested_value(result, target_path, _convert_name_style(current_val, style))
|
||
|
||
return result
|
||
|
||
|
||
# ==============================================================================
|
||
# 请求构建器
|
||
# ==============================================================================
|
||
|
||
|
||
class RequestBuilder(ABC):
|
||
"""请求构建器抽象基类"""
|
||
|
||
@abstractmethod
|
||
def build_payload(
|
||
self,
|
||
original_body: dict[str, Any],
|
||
*,
|
||
mapped_model: str | None = None,
|
||
is_stream: bool = False,
|
||
) -> dict[str, Any]:
|
||
"""构建请求体"""
|
||
pass
|
||
|
||
@abstractmethod
|
||
def build_headers(
|
||
self,
|
||
original_headers: dict[str, str],
|
||
endpoint: Any,
|
||
key: Any,
|
||
*,
|
||
extra_headers: dict[str, str] | None = None,
|
||
pre_computed_auth: tuple[str, str] | None = None,
|
||
envelope: ProviderEnvelope | None = None,
|
||
body: dict[str, Any] | None = None,
|
||
original_body: dict[str, Any] | None = None,
|
||
) -> dict[str, str]:
|
||
"""构建请求头"""
|
||
pass
|
||
|
||
def build(
|
||
self,
|
||
original_body: dict[str, Any],
|
||
original_headers: dict[str, str],
|
||
endpoint: Any,
|
||
key: Any,
|
||
*,
|
||
mapped_model: str | None = None,
|
||
is_stream: bool = False,
|
||
extra_headers: dict[str, str] | None = None,
|
||
pre_computed_auth: tuple[str, str] | None = None,
|
||
envelope: ProviderEnvelope | None = None,
|
||
provider_api_format: str | None = None,
|
||
) -> tuple[dict[str, Any], dict[str, str]]:
|
||
"""
|
||
构建完整的请求(请求体 + 请求头)
|
||
|
||
Args:
|
||
original_body: 原始请求体
|
||
original_headers: 原始请求头
|
||
endpoint: 端点配置
|
||
key: Provider API Key
|
||
mapped_model: 映射后的模型名
|
||
is_stream: 是否为流式请求
|
||
extra_headers: 额外请求头
|
||
pre_computed_auth: 预先计算的认证信息 (auth_header, auth_value)
|
||
provider_api_format: 运行时实际生效的 Provider API 格式,用于准确记录摘要日志
|
||
|
||
Returns:
|
||
Tuple[payload, headers]
|
||
"""
|
||
payload = self.build_payload(
|
||
original_body,
|
||
mapped_model=mapped_model,
|
||
is_stream=is_stream,
|
||
)
|
||
|
||
# 应用请求体规则(如果 endpoint 配置了 body_rules)
|
||
body_rules = getattr(endpoint, "body_rules", None)
|
||
if body_rules:
|
||
payload = apply_body_rules(
|
||
payload,
|
||
body_rules,
|
||
original_body=original_body,
|
||
)
|
||
|
||
effective_provider_api_format = provider_api_format or getattr(endpoint, "api_format", None)
|
||
logger.debug(
|
||
"[RequestBuilder] outbound payload summary: {}",
|
||
summarize_request_payload_shape(
|
||
payload,
|
||
provider_api_format=effective_provider_api_format,
|
||
body_rules=body_rules,
|
||
),
|
||
)
|
||
|
||
headers = self.build_headers(
|
||
original_headers,
|
||
endpoint,
|
||
key,
|
||
extra_headers=extra_headers,
|
||
pre_computed_auth=pre_computed_auth,
|
||
envelope=envelope,
|
||
body=payload,
|
||
original_body=original_body,
|
||
)
|
||
return payload, headers
|
||
|
||
|
||
class PassthroughRequestBuilder(RequestBuilder):
|
||
"""
|
||
透传模式请求构建器
|
||
|
||
适用于 CLI 等场景,尽量保持请求原样:
|
||
- 请求体:直接复制,只修改必要字段(model, stream)
|
||
- 请求头:清理敏感头部(黑名单),透传其他所有头部
|
||
"""
|
||
|
||
def build_payload(
|
||
self,
|
||
original_body: dict[str, Any],
|
||
*,
|
||
mapped_model: str | None = None,
|
||
is_stream: bool = False,
|
||
) -> dict[str, Any]:
|
||
"""
|
||
透传请求体 - 原样复制,不做任何修改
|
||
|
||
透传模式下:
|
||
- model: 由各 handler 的 apply_mapped_model 方法处理
|
||
- stream: 保留客户端原始值(不同 API 处理方式不同)
|
||
"""
|
||
del mapped_model, is_stream
|
||
return dict(original_body)
|
||
|
||
@staticmethod
|
||
def _merge_comma_header_values(primary: str, secondary: str) -> str:
|
||
"""合并逗号分隔 header 值并去重,保持 primary 在前。"""
|
||
seen: set[str] = set()
|
||
merged: list[str] = []
|
||
|
||
def _append(raw: str) -> None:
|
||
for token in str(raw or "").split(","):
|
||
token = token.strip()
|
||
if not token or token in seen:
|
||
continue
|
||
seen.add(token)
|
||
merged.append(token)
|
||
|
||
_append(primary)
|
||
_append(secondary)
|
||
return ",".join(merged)
|
||
|
||
@classmethod
|
||
def _drop_beta_token(cls, value: str, token: str) -> str:
|
||
"""从逗号分隔 header 中移除指定 token。"""
|
||
if not value or token not in value:
|
||
return value
|
||
return cls._merge_comma_header_values(
|
||
",".join(p.strip() for p in str(value).split(",") if p.strip() and p.strip() != token),
|
||
"",
|
||
)
|
||
|
||
@classmethod
|
||
def _merge_extra_headers_with_original(
|
||
cls,
|
||
original_headers: dict[str, str],
|
||
extra_headers: dict[str, str] | None,
|
||
*,
|
||
envelope: ProviderEnvelope | None = None,
|
||
) -> dict[str, str] | None:
|
||
"""合并 extra_headers 与原始头部中的特定字段。"""
|
||
if not extra_headers:
|
||
return None
|
||
|
||
merged_extra = dict(extra_headers)
|
||
beta_extra_key = next((k for k in merged_extra if k.lower() == "anthropic-beta"), None)
|
||
if beta_extra_key is None:
|
||
return merged_extra
|
||
|
||
incoming_beta = next(
|
||
(v for k, v in original_headers.items() if k.lower() == "anthropic-beta"),
|
||
"",
|
||
)
|
||
merged_beta = str(merged_extra.get(beta_extra_key) or "")
|
||
if incoming_beta:
|
||
merged_beta = cls._merge_comma_header_values(
|
||
merged_beta,
|
||
str(incoming_beta),
|
||
)
|
||
|
||
# 由 envelope 声明需要排除的 beta token(如 Claude Code OAuth 的 context-1m)。
|
||
if envelope and hasattr(envelope, "excluded_beta_tokens"):
|
||
for token in envelope.excluded_beta_tokens():
|
||
merged_beta = cls._drop_beta_token(merged_beta, token)
|
||
|
||
merged_extra[beta_extra_key] = merged_beta
|
||
return merged_extra
|
||
|
||
def build_headers(
|
||
self,
|
||
original_headers: dict[str, str],
|
||
endpoint: Any,
|
||
key: Any,
|
||
*,
|
||
extra_headers: dict[str, str] | None = None,
|
||
pre_computed_auth: tuple[str, str] | None = None,
|
||
envelope: ProviderEnvelope | None = None,
|
||
body: dict[str, Any] | None = None,
|
||
original_body: dict[str, Any] | None = None,
|
||
) -> dict[str, str]:
|
||
"""
|
||
透传请求头 - 清理敏感头部(黑名单),透传其他所有头部
|
||
|
||
Args:
|
||
original_headers: 原始请求头
|
||
endpoint: 端点配置
|
||
key: Provider API Key
|
||
extra_headers: 额外请求头
|
||
pre_computed_auth: 预先计算的认证信息 (auth_header, auth_value),
|
||
用于 Service Account 等异步获取 token 的场景
|
||
"""
|
||
raw_family = getattr(endpoint, "api_family", None)
|
||
raw_kind = getattr(endpoint, "endpoint_kind", None)
|
||
endpoint_sig: str | None = None
|
||
if isinstance(raw_family, str) and isinstance(raw_kind, str) and raw_family and raw_kind:
|
||
endpoint_sig = make_signature_key(raw_family, raw_kind)
|
||
else:
|
||
# 兜底:允许 endpoint.api_format 已经是 signature key 的情况
|
||
raw_format = getattr(endpoint, "api_format", None)
|
||
if isinstance(raw_format, str) and ":" in raw_format:
|
||
endpoint_sig = raw_format
|
||
|
||
# 1. 根据 API 格式自动设置认证头
|
||
if pre_computed_auth:
|
||
# 使用预先计算的认证信息(Service Account 等场景)
|
||
auth_header, auth_value = pre_computed_auth
|
||
else:
|
||
# 标准 API Key 认证
|
||
decrypted_key = crypto_service.decrypt(key.api_key)
|
||
|
||
auth_header, auth_type = get_auth_config_for_endpoint(endpoint_sig or "openai:chat")
|
||
auth_value = f"Bearer {decrypted_key}" if auth_type == "bearer" else decrypted_key
|
||
# 认证头始终受保护,防止 header_rules 覆盖
|
||
protected_keys = {auth_header.lower(), "content-type"}
|
||
|
||
builder = HeaderBuilder()
|
||
|
||
# 2. 透传原始头部(排除默认敏感头部)
|
||
if original_headers:
|
||
for name, value in original_headers.items():
|
||
if name.lower() in SENSITIVE_HEADERS:
|
||
continue
|
||
builder.add(name, value)
|
||
|
||
# 3. 应用 endpoint 的请求头规则(认证头受保护,无法通过 rules 设置)
|
||
header_rules = getattr(endpoint, "header_rules", None)
|
||
if header_rules:
|
||
builder.apply_rules(
|
||
header_rules,
|
||
protected_keys,
|
||
body=body,
|
||
original_body=original_body,
|
||
condition_evaluator=evaluate_condition,
|
||
)
|
||
|
||
# 4. 添加额外头部
|
||
effective_extra_headers = self._merge_extra_headers_with_original(
|
||
original_headers,
|
||
extra_headers,
|
||
envelope=envelope,
|
||
)
|
||
if effective_extra_headers:
|
||
builder.add_many(effective_extra_headers)
|
||
|
||
# 5. 设置认证头(最高优先级,上游始终使用 header 认证)
|
||
builder.add(resolve_header_name_case(original_headers, 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
|
||
|
||
|
||
# ==============================================================================
|
||
# 便捷函数
|
||
# ==============================================================================
|
||
|
||
|
||
def build_passthrough_request(
|
||
original_body: dict[str, Any],
|
||
original_headers: dict[str, str],
|
||
endpoint: Any,
|
||
key: Any,
|
||
) -> tuple[dict[str, Any], dict[str, str]]:
|
||
"""
|
||
构建透传模式的请求
|
||
|
||
纯透传:原样复制请求体,只处理请求头(认证等)。
|
||
model mapping 和 stream 由调用方自行处理(不同 API 格式处理方式不同)。
|
||
"""
|
||
builder = PassthroughRequestBuilder()
|
||
return builder.build(
|
||
original_body,
|
||
original_headers,
|
||
endpoint,
|
||
key,
|
||
)
|