Files
Aether/_deprecated_py_src/api/handlers/base/request_builder.py
fawney19 1d9c77522a refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构
- 删除全部 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)
2026-04-03 16:26:16 +08:00

1417 lines
47 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
请求构建器 - 透传模式
透传模式 (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 keyint 表示数组索引_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,
)