Files
Aether/_deprecated_py_src/api/handlers/base/request_builder.py

1417 lines
47 KiB
Python
Raw Normal View History

2025-12-10 20:52:44 +08:00
"""
请求构建器 - 透传模式
透传模式 (Passthrough): CLI Chat 等场景原样转发请求体和头部
- 清理敏感头部authorization, x-api-key, host, content-length
- 保留所有其他头部和请求体字段
- 适用于Claude CLIOpenAI CLIChat API 等场景
使用方式
builder = PassthroughRequestBuilder()
payload, headers = builder.build(original_body, original_headers, endpoint, key)
"""
from __future__ import annotations
2025-12-10 20:52:44 +08:00
import copy
import re
2025-12-10 20:52:44 +08:00
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any
2025-12-10 20:52:44 +08:00
from src.core.api_format import (
UPSTREAM_DROP_HEADERS,
HeaderBuilder,
get_auth_config_for_endpoint,
make_signature_key,
resolve_header_name_case,
)
2025-12-10 20:52:44 +08:00
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,
}
2025-12-10 20:52:44 +08:00
# ==============================================================================
# 统一的头部配置常量
# ==============================================================================
# 兼容别名:历史代码使用 SENSITIVE_HEADERS 命名
SENSITIVE_HEADERS: frozenset[str] = UPSTREAM_DROP_HEADERS
2025-12-10 20:52:44 +08:00
# ==============================================================================
# 测试请求常量与辅助函数
# ==============================================================================
# 标准测试请求体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
2025-12-10 20:52:44 +08:00
# ==============================================================================
# 请求构建器
# ==============================================================================
class RequestBuilder(ABC):
"""请求构建器抽象基类"""
@abstractmethod
def build_payload(
self,
original_body: dict[str, Any],
2025-12-10 20:52:44 +08:00
*,
mapped_model: str | None = None,
2025-12-10 20:52:44 +08:00
is_stream: bool = False,
) -> dict[str, Any]:
2025-12-10 20:52:44 +08:00
"""构建请求体"""
pass
@abstractmethod
def build_headers(
self,
original_headers: dict[str, str],
2025-12-10 20:52:44 +08:00
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]:
2025-12-10 20:52:44 +08:00
"""构建请求头"""
pass
def build(
self,
original_body: dict[str, Any],
original_headers: dict[str, str],
2025-12-10 20:52:44 +08:00
endpoint: Any,
key: Any,
*,
mapped_model: str | None = None,
2025-12-10 20:52:44 +08:00
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]]:
2025-12-10 20:52:44 +08:00
"""
构建完整的请求请求体 + 请求头
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 格式用于准确记录摘要日志
2025-12-10 20:52:44 +08:00
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,
),
)
2025-12-10 20:52:44 +08:00
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,
2025-12-10 20:52:44 +08:00
)
return payload, headers
class PassthroughRequestBuilder(RequestBuilder):
"""
透传模式请求构建器
适用于 CLI 等场景尽量保持请求原样
- 请求体直接复制只修改必要字段model, stream
- 请求头清理敏感头部黑名单透传其他所有头部
"""
def build_payload(
self,
original_body: dict[str, Any],
2025-12-10 20:52:44 +08:00
*,
mapped_model: str | None = None,
is_stream: bool = False,
) -> dict[str, Any]:
2025-12-10 20:52:44 +08:00
"""
透传请求体 - 原样复制不做任何修改
透传模式下
- model: 由各 handler apply_mapped_model 方法处理
- stream: 保留客户端原始值不同 API 处理方式不同
"""
del mapped_model, is_stream
2025-12-10 20:52:44 +08:00
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
2025-12-10 20:52:44 +08:00
def build_headers(
self,
original_headers: dict[str, str],
2025-12-10 20:52:44 +08:00
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]:
2025-12-10 20:52:44 +08:00
"""
透传请求头 - 清理敏感头部黑名单透传其他所有头部
Args:
original_headers: 原始请求头
endpoint: 端点配置
key: Provider API Key
extra_headers: 额外请求头
pre_computed_auth: 预先计算的认证信息 (auth_header, auth_value)
用于 Service Account 等异步获取 token 的场景
2025-12-10 20:52:44 +08:00
"""
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
2025-12-10 20:52:44 +08:00
# 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"}
2025-12-10 20:52:44 +08:00
builder = HeaderBuilder()
2025-12-10 20:52:44 +08:00
# 2. 透传原始头部(排除默认敏感头部)
2025-12-10 20:52:44 +08:00
if original_headers:
for name, value in original_headers.items():
if name.lower() in SENSITIVE_HEADERS:
2025-12-10 20:52:44 +08:00
continue
builder.add(name, value)
2025-12-10 20:52:44 +08:00
# 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,
)
2025-12-10 20:52:44 +08:00
# 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)
2025-12-10 20:52:44 +08:00
# 6. 确保有 Content-Type
headers = builder.build()
if not any(k.lower() == "content-type" for k in headers):
2025-12-10 20:52:44 +08:00
headers["Content-Type"] = "application/json"
return headers
# ==============================================================================
# 便捷函数
# ==============================================================================
def build_passthrough_request(
original_body: dict[str, Any],
original_headers: dict[str, str],
2025-12-10 20:52:44 +08:00
endpoint: Any,
key: Any,
) -> tuple[dict[str, Any], dict[str, str]]:
2025-12-10 20:52:44 +08:00
"""
构建透传模式的请求
纯透传原样复制请求体只处理请求头认证等
model mapping stream 由调用方自行处理不同 API 格式处理方式不同
"""
builder = PassthroughRequestBuilder()
return builder.build(
original_body,
original_headers,
endpoint,
key,
)