Files
Aether/_deprecated_py_src/models/endpoint_models.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

1279 lines
49 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.
"""
ProviderEndpoint 相关的 API 模型定义
"""
from __future__ import annotations
import re
from datetime import datetime
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator
from src.models.admin_requests import (
ClaudeCodeAdvancedConfig,
FailoverRulesConfig,
PoolAdvancedConfig,
ProxyConfig,
)
from src.models.status_snapshot import ProviderKeyStatusSnapshotResponse
# ========== Header Rule 类型定义 ==========
# 请求头规则支持三种操作:
# - set: 设置/覆盖请求头 {"action": "set", "key": "X-Custom", "value": "val"}
# - drop: 删除请求头 {"action": "drop", "key": "X-Unwanted"}
# - rename: 重命名请求头 {"action": "rename", "from": "X-Old", "to": "X-New"}
# 实际验证在 headers.py 的 apply_rules 中处理
HeaderRule = dict[str, Any]
_HEADER_RULE_ACTIONS: frozenset[str] = frozenset({"set", "drop", "rename"})
# ========== Body Rule 类型定义 ==========
# 请求体规则支持六种操作:
# - set: 设置/覆盖字段 {"action": "set", "path": "metadata", "value": {"custom": "val"}}
# 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": "...", "pattern": "...", "replacement": "..."}
# - name_style: 转换命名风格 {"action": "name_style", "path": "tools[*].name", "style": "camelCase"}
# 路径语法支持数组索引messages[0].content, data[-1], matrix[0][1]
# 路径语法支持通配符tools[*].name遍历所有元素, tools[0-4].name遍历范围
# 运行时处理在 request_builder.py 的 apply_body_rules 中;结构校验见 _validate_body_rules
BodyRule = dict[str, Any]
# body_rules 允许的 action 集合
_BODY_RULE_ACTIONS: frozenset[str] = frozenset(
{"set", "drop", "rename", "append", "insert", "regex_replace", "name_style"}
)
# name_style 允许的风格值
_NAME_STYLE_VALUES: frozenset[str] = frozenset(
{"snake_case", "camelCase", "PascalCase", "kebab-case", "capitalize"}
)
# regex_replace 允许的 flags 字符
_REGEX_FLAG_CHARS: frozenset[str] = frozenset({"i", "m", "s"})
# condition 允许的操作符
_CONDITION_OPS: frozenset[str] = frozenset(
{
"eq",
"neq",
"gt",
"lt",
"gte",
"lte",
"starts_with",
"ends_with",
"contains",
"matches",
"exists",
"not_exists",
"in",
"type_is",
}
)
# type_is 允许的类型值
_TYPE_IS_VALUES: frozenset[str] = frozenset(
{"string", "number", "boolean", "array", "object", "null"}
)
_CONDITION_SOURCES: frozenset[str] = frozenset({"current", "original"})
def parse_re_flags(flags_str: str) -> int:
"""将 flags 字符串i/m/s转换为 re 标志位。
供 endpoint_models 校验和 request_builder 运行时共用。
"""
result = 0
for f in flags_str:
if f == "i":
result |= re.IGNORECASE
elif f == "m":
result |= re.MULTILINE
elif f == "s":
result |= re.DOTALL
return result
def _validate_condition(condition: Any, rule_label: str) -> None:
"""校验单条规则的 condition 结构。"""
if not isinstance(condition, dict):
raise ValueError(f"{rule_label}: condition 必须是 JSON 对象")
has_all = "all" in condition
has_any = "any" in condition
if has_all or has_any:
if has_all and has_any:
raise ValueError(f"{rule_label}: condition 不能同时包含 all 和 any")
key = "all" if has_all else "any"
children = condition.get(key)
if not isinstance(children, list) or not children:
raise ValueError(f"{rule_label}: condition.{key} 必须是非空数组")
for idx, child in enumerate(children):
_validate_condition(child, f"{rule_label}: condition.{key}[{idx}]")
return
source = condition.get("source", "current")
if not isinstance(source, str) or source not in _CONDITION_SOURCES:
raise ValueError(
f"{rule_label}: condition.source 必须是 {sorted(_CONDITION_SOURCES)} 之一,"
f"当前值: {source!r}"
)
op = condition.get("op")
if not isinstance(op, str) or op not in _CONDITION_OPS:
raise ValueError(
f"{rule_label}: condition.op 必须是 {sorted(_CONDITION_OPS)} 之一," f"当前值: {op!r}"
)
path = condition.get("path")
if not isinstance(path, str) or not path.strip():
raise ValueError(f"{rule_label}: condition 必须提供非空 path")
# exists / not_exists 不需要 value
if op in ("exists", "not_exists"):
return
value = condition.get("value")
# 数值操作符校验
if op in ("gt", "lt", "gte", "lte"):
if not isinstance(value, (int, float)) or isinstance(value, bool):
raise ValueError(f"{rule_label}: condition op={op!r} 的 value 必须为数值")
# matches 正则校验
if op == "matches":
if not isinstance(value, str) or not value:
raise ValueError(f"{rule_label}: condition op=matches 的 value 必须为非空字符串")
try:
re.compile(value)
except re.error as e:
raise ValueError(f"{rule_label}: condition op=matches 的 value 不是合法正则: {e}")
# in 校验
if op == "in":
if not isinstance(value, list):
raise ValueError(f"{rule_label}: condition op=in 的 value 必须为数组")
# type_is 校验
if op == "type_is":
if not isinstance(value, str) or value not in _TYPE_IS_VALUES:
raise ValueError(
f"{rule_label}: condition op=type_is 的 value 必须是 "
f"{sorted(_TYPE_IS_VALUES)} 之一"
)
# starts_with / ends_with / contains 对 value 做字符串校验
if op in ("starts_with", "ends_with"):
if not isinstance(value, str):
raise ValueError(f"{rule_label}: condition op={op!r} 的 value 必须为字符串")
def _validate_header_rules(rules: list[HeaderRule]) -> list[HeaderRule]:
"""校验 header_rules 列表的结构和 condition 合法性。"""
for idx, rule in enumerate(rules):
if not isinstance(rule, dict):
raise ValueError(f"header_rules[{idx}]: 规则必须是 JSON 对象")
action = rule.get("action")
if not isinstance(action, str) or action.strip().lower() not in _HEADER_RULE_ACTIONS:
raise ValueError(
f"header_rules[{idx}]: action 必须是 {sorted(_HEADER_RULE_ACTIONS)} 之一,"
f"当前值: {action!r}"
)
action = action.strip().lower()
if action == "set":
key = rule.get("key")
value = rule.get("value")
if not isinstance(key, str) or not key.strip():
raise ValueError(f"header_rules[{idx}]: set 必须提供非空 key")
if not isinstance(value, str):
raise ValueError(f"header_rules[{idx}]: set 的 value 必须为字符串")
if action == "drop":
key = rule.get("key")
if not isinstance(key, str) or not key.strip():
raise ValueError(f"header_rules[{idx}]: drop 必须提供非空 key")
if action == "rename":
from_val = rule.get("from")
to_val = rule.get("to")
if not isinstance(from_val, str) or not from_val.strip():
raise ValueError(f"header_rules[{idx}]: rename 必须提供非空 from")
if not isinstance(to_val, str) or not to_val.strip():
raise ValueError(f"header_rules[{idx}]: rename 必须提供非空 to")
condition = rule.get("condition")
if condition is not None:
_validate_condition(condition, f"header_rules[{idx}]")
return rules
def _validate_body_rules(rules: list[BodyRule]) -> list[BodyRule]:
"""校验 body_rules 列表的结构和正则合法性。
校验项:
- 每条规则必须是 dict 且包含合法 action
- 需要 path 的 actionset/drop/append/insert/regex_replace必须提供非空 path 字符串
- rename 必须提供非空 from / to 字符串
- insert 的 index 必须为整数
- regex_replace 的 pattern 必须能通过 re.compile 编译flags 仅允许 i/m/s
"""
for idx, rule in enumerate(rules):
if not isinstance(rule, dict):
raise ValueError(f"body_rules[{idx}]: 规则必须是 JSON 对象")
action = rule.get("action")
if not isinstance(action, str) or action.strip().lower() not in _BODY_RULE_ACTIONS:
raise ValueError(
f"body_rules[{idx}]: action 必须是 {sorted(_BODY_RULE_ACTIONS)} 之一,"
f"当前值: {action!r}"
)
action = action.strip().lower()
# ---------- path 校验 ----------
if action in {"set", "drop", "append", "insert", "regex_replace", "name_style"}:
path = rule.get("path")
if not isinstance(path, str) or not path.strip():
raise ValueError(f"body_rules[{idx}]: action={action!r} 必须提供非空 path")
# ---------- rename 校验 ----------
if action == "rename":
from_val = rule.get("from")
to_val = rule.get("to")
if not isinstance(from_val, str) or not from_val.strip():
raise ValueError(f"body_rules[{idx}]: rename 必须提供非空 from")
if not isinstance(to_val, str) or not to_val.strip():
raise ValueError(f"body_rules[{idx}]: rename 必须提供非空 to")
# ---------- insert 校验 ----------
if action == "insert":
index = rule.get("index")
if not isinstance(index, int) or isinstance(index, bool):
raise ValueError(f"body_rules[{idx}]: insert 的 index 必须为整数")
# ---------- regex_replace 校验 ----------
if action == "regex_replace":
pattern = rule.get("pattern")
if not isinstance(pattern, str) or not pattern:
raise ValueError(f"body_rules[{idx}]: regex_replace 必须提供非空 pattern 字符串")
replacement = rule.get("replacement", "")
if not isinstance(replacement, str):
raise ValueError(f"body_rules[{idx}]: regex_replace 的 replacement 必须为字符串")
# 校验 flags
flags_str = rule.get("flags", "")
re_flags = 0
if isinstance(flags_str, str) and flags_str:
invalid_flags = set(flags_str) - _REGEX_FLAG_CHARS
if invalid_flags:
raise ValueError(
f"body_rules[{idx}]: regex_replace 的 flags 仅允许 "
f"{''.join(sorted(_REGEX_FLAG_CHARS))}"
f"非法字符: {''.join(sorted(invalid_flags))}"
)
re_flags = parse_re_flags(flags_str)
# 尝试编译正则,捕获语法错误
try:
re.compile(pattern, re_flags)
except re.error as e:
raise ValueError(
f"body_rules[{idx}]: regex_replace 的 pattern 不是合法正则表达式: {e}"
)
# 校验 count
count = rule.get("count", 0)
if not isinstance(count, int) or count < 0:
raise ValueError(f"body_rules[{idx}]: regex_replace 的 count 必须为非负整数")
# ---------- name_style 校验 ----------
if action == "name_style":
style = rule.get("style")
if not isinstance(style, str) or style not in _NAME_STYLE_VALUES:
raise ValueError(
f"body_rules[{idx}]: name_style 的 style 必须是 "
f"{sorted(_NAME_STYLE_VALUES)} 之一,当前值: {style!r}"
)
# ---------- condition 校验 ----------
condition = rule.get("condition")
if condition is not None:
_validate_condition(condition, f"body_rules[{idx}]")
return rules
# ========== ProviderEndpoint CRUD ==========
class ProviderEndpointCreate(BaseModel):
"""创建 Endpoint 请求"""
provider_id: str = Field(..., description="Provider ID")
api_format: str = Field(
...,
description=(
"Endpoint signature例如: claude:chat/claude:cli, openai:chat/openai:cli/openai:compact/openai:video, gemini:chat/gemini:cli/gemini:video"
),
)
base_url: str = Field(..., min_length=1, max_length=500, description="API 基础 URL")
custom_path: str | None = Field(default=None, max_length=200, description="自定义请求路径")
# 请求头配置
header_rules: list[HeaderRule] | None = Field(
default=None,
description="请求头规则列表,支持 set/drop/rename 操作",
)
# 请求体配置
body_rules: list[BodyRule] | None = Field(
default=None,
description="请求体规则列表,支持 set/drop/rename/append/insert/regex_replace 操作",
)
max_retries: int = Field(default=2, ge=0, le=999, description="最大重试次数")
# 额外配置
config: dict[str, Any] | None = Field(default=None, description="额外配置JSON")
# 代理配置
proxy: ProxyConfig | None = Field(default=None, description="代理配置")
# 格式转换配置
format_acceptance_config: dict[str, Any] | None = Field(
default=None,
description="格式接受策略配置(跨格式转换开关/白黑名单等)",
)
@field_validator("api_format")
@classmethod
def validate_api_format(cls, v: str) -> str:
"""验证 API 格式"""
from src.core.api_format import (
list_endpoint_definitions,
resolve_endpoint_definition,
)
from src.core.api_format.signature import normalize_signature_key
normalized = normalize_signature_key(v)
if resolve_endpoint_definition(normalized) is None:
allowed = [d.signature_key for d in list_endpoint_definitions()]
raise ValueError(f"api_format 必须是以下之一: {allowed}")
return normalized
@field_validator("base_url")
@classmethod
def validate_base_url(cls, v: str) -> str:
if not re.match(r"^https?://", v, re.IGNORECASE):
raise ValueError("URL 必须以 http:// 或 https:// 开头")
return v.rstrip("/") # 移除末尾斜杠
@field_validator("body_rules")
@classmethod
def validate_body_rules(cls, v: list[BodyRule] | None) -> list[BodyRule] | None:
"""校验 body_rules 结构和正则合法性"""
if v is None:
return v
return _validate_body_rules(v)
@field_validator("header_rules")
@classmethod
def validate_header_rules(cls, v: list[HeaderRule] | None) -> list[HeaderRule] | None:
"""校验 header_rules 结构和 condition 合法性"""
if v is None:
return v
return _validate_header_rules(v)
class ProviderEndpointUpdate(BaseModel):
"""更新 Endpoint 请求"""
base_url: str | None = Field(
default=None, min_length=1, max_length=500, description="API 基础 URL"
)
custom_path: str | None = Field(default=None, max_length=200, description="自定义请求路径")
# 请求头配置
header_rules: list[HeaderRule] | None = Field(
default=None,
description="请求头规则列表,支持 set/drop/rename 操作",
)
# 请求体配置
body_rules: list[BodyRule] | None = Field(
default=None,
description="请求体规则列表,支持 set/drop/rename/append/insert/regex_replace 操作",
)
max_retries: int | None = Field(default=None, ge=0, le=999, description="最大重试次数")
is_active: bool | None = Field(default=None, description="是否启用")
config: dict[str, Any] | None = Field(default=None, description="额外配置")
proxy: ProxyConfig | None = Field(default=None, description="代理配置")
# 格式转换配置
format_acceptance_config: dict[str, Any] | None = Field(
default=None,
description="格式接受策略配置(跨格式转换开关/白黑名单等)",
)
@field_validator("base_url")
@classmethod
def validate_base_url(cls, v: str | None) -> str | None:
"""验证 API URL"""
if v is None:
return v
if not re.match(r"^https?://", v, re.IGNORECASE):
raise ValueError("URL 必须以 http:// 或 https:// 开头")
return v.rstrip("/") # 移除末尾斜杠
@field_validator("body_rules")
@classmethod
def validate_body_rules(cls, v: list[BodyRule] | None) -> list[BodyRule] | None:
"""校验 body_rules 结构和正则合法性"""
if v is None:
return v
return _validate_body_rules(v)
@field_validator("header_rules")
@classmethod
def validate_header_rules(cls, v: list[HeaderRule] | None) -> list[HeaderRule] | None:
"""校验 header_rules 结构和 condition 合法性"""
if v is None:
return v
return _validate_header_rules(v)
class ProviderEndpointResponse(BaseModel):
"""Endpoint 响应"""
id: str
provider_id: str
provider_name: str # 冗余字段,方便前端显示
# API 配置
api_format: str
base_url: str
custom_path: str | None = None
# 请求头配置
header_rules: list[HeaderRule] | None = Field(default=None, description="请求头规则列表")
# 请求体配置
body_rules: list[BodyRule] | None = Field(default=None, description="请求体规则列表")
max_retries: int
# 状态
is_active: bool
# 额外配置
config: dict[str, Any] | None = None
# 代理配置(响应中密码已脱敏)
proxy: dict[str, Any] | None = Field(default=None, description="代理配置(密码已脱敏)")
# 格式转换配置
format_acceptance_config: dict[str, Any] | None = Field(
default=None,
description="格式接受策略配置(跨格式转换开关/白黑名单等)",
)
# 统计(从 Keys 聚合)
total_keys: int = Field(default=0, description="总 Key 数量")
active_keys: int = Field(default=0, description="活跃 Key 数量")
# 时间戳
created_at: datetime
updated_at: datetime
model_config = ConfigDict(from_attributes=True)
# ========== ProviderAPIKey 相关 ==========
class EndpointAPIKeyCreate(BaseModel):
"""为 Provider 添加 API Key"""
provider_id: str | None = Field(default=None, description="Provider ID从 URL 获取)")
api_formats: list[str] | None = Field(
default=None,
min_length=1,
description="支持的 endpoint signature 列表(必填,路由层校验)",
)
api_key: str = Field(
default="",
max_length=10000,
description="API Key标准认证时必填将自动加密",
)
auth_type: Literal["api_key", "service_account", "oauth"] = Field(
default="api_key",
description="认证类型api_key标准 API Key/ service_accountGCP Service Account/ oauthOAuth access_token",
)
auth_config: dict[str, Any] | None = Field(
default=None,
description=(
"认证配置JSONservice_account 时存储完整 Service Account JSON"
"oauth 时存储 token/refresh/expires_at 等(后端加密存储,不在响应中返回)"
),
)
name: str = Field(..., min_length=1, max_length=100, description="密钥名称(必填,用于识别)")
# 成本计算
rate_multipliers: dict[str, float] | None = Field(
default=None,
description="按 endpoint signature 的成本倍率,如 {'claude:cli': 1.0, 'openai:cli': 0.8}",
)
# 优先级和限制(数字越小越优先)
internal_priority: int = Field(default=50, description="Key 内部优先级(提供商优先模式)")
# rpm_limit: NULL=自适应模式(系统自动学习),数字=固定限制模式
rpm_limit: int | None = Field(
default=None, ge=1, le=10000, description="RPM 限制NULL=自适应模式)"
)
allowed_models: list[str] | None = Field(
default=None,
description="允许使用的模型列表null=不限制)",
)
# 能力标签
capabilities: dict[str, bool] | None = Field(
default=None,
description="Key 能力标签,如 {'cache_1h': true, 'context_1m': true}",
)
# 缓存与熔断配置
cache_ttl_minutes: int = Field(
default=5, ge=0, le=60, description="缓存 TTL分钟0=禁用默认5分钟"
)
max_probe_interval_minutes: int = Field(
default=32, ge=0, le=32, description="熔断探测间隔(分钟),范围 0-32"
)
# 备注
note: str | None = Field(default=None, max_length=500, description="备注说明(可选)")
# 自动获取模型
auto_fetch_models: bool = Field(
default=False,
description="是否启用自动获取模型(启用后系统定时从上游 API 获取可用模型)",
)
# 锁定的模型列表
locked_models: list[str] | None = Field(
default=None, description="被锁定的模型列表(刷新时不会被删除)"
)
# 模型过滤规则(仅当 auto_fetch_models=True 时生效)
model_include_patterns: list[str] | None = Field(
default=None, description="模型包含规则(支持 * 和 ? 通配符),空表示包含所有"
)
model_exclude_patterns: list[str] | None = Field(
default=None, description="模型排除规则(支持 * 和 ? 通配符),空表示不排除"
)
@field_validator("api_formats")
@classmethod
def validate_api_formats(cls, v: list[str] | None) -> list[str] | None:
"""验证 API 格式列表"""
if v is None:
return v
from src.core.api_format import (
list_endpoint_definitions,
resolve_endpoint_definition,
)
from src.core.api_format.signature import normalize_signature_key
allowed = [d.signature_key for d in list_endpoint_definitions()]
validated: list[str] = []
seen: set[str] = set()
for fmt in v:
normalized = normalize_signature_key(fmt)
if resolve_endpoint_definition(normalized) is None:
raise ValueError(f"api_formats 必须是以下之一: {allowed},当前值: {fmt}")
if normalized in seen:
continue # 静默去重
seen.add(normalized)
validated.append(normalized)
return validated
@field_validator("allowed_models")
@classmethod
def validate_allowed_models(cls, v: list[str] | None) -> list[str] | None:
"""
规范化 allowed_models去空、去重、保留顺序
"""
if v is None:
return v
if not isinstance(v, list):
raise ValueError("allowed_models 必须是列表")
cleaned: list[str] = []
seen: set[str] = set()
for item in v:
if not isinstance(item, str):
raise ValueError("allowed_models 列表元素必须为字符串")
name = item.strip()
if not name or name in seen:
continue
seen.add(name)
cleaned.append(name)
return cleaned
@field_validator("api_key")
@classmethod
def validate_api_key(cls, v: str) -> str:
"""验证 API Key 基本格式"""
# 移除首尾空白(长度校验由 Field max_length 处理)
return v.strip()
@field_validator("name")
@classmethod
def validate_name(cls, v: str) -> str:
"""验证名称(防止 XSS"""
# 移除危险的 HTML 标签
v = re.sub(r"<script.*?</script>", "", v, flags=re.IGNORECASE | re.DOTALL)
v = re.sub(r"<iframe.*?</iframe>", "", v, flags=re.IGNORECASE | re.DOTALL)
v = re.sub(r"javascript:", "", v, flags=re.IGNORECASE)
v = re.sub(r"on\w+\s*=", "", v, flags=re.IGNORECASE)
return v.strip()
@field_validator("note")
@classmethod
def validate_note(cls, v: str | None) -> str | None:
"""验证备注(防止 XSS"""
if v is None:
return v
# 移除危险的 HTML 标签
v = re.sub(r"<script.*?</script>", "", v, flags=re.IGNORECASE | re.DOTALL)
v = re.sub(r"<iframe.*?</iframe>", "", v, flags=re.IGNORECASE | re.DOTALL)
v = re.sub(r"javascript:", "", v, flags=re.IGNORECASE)
v = re.sub(r"on\w+\s*=", "", v, flags=re.IGNORECASE)
return v.strip()
class EndpointAPIKeyUpdate(BaseModel):
"""更新 Endpoint API Key"""
api_formats: list[str] | None = Field(
default=None, min_length=1, description="支持的 API 格式列表"
)
api_key: str | None = Field(
default=None,
max_length=10000,
description="API Key标准认证时使用将自动加密",
)
auth_type: Literal["api_key", "service_account", "oauth"] | None = Field(
default=None,
description="认证类型api_key标准 API Key/ service_accountGCP Service Account/ oauthOAuth access_token",
)
auth_config: dict[str, Any] | None = Field(
default=None,
description=(
"认证配置JSONservice_account 时存储完整 Service Account JSON"
"oauth 时存储 token/refresh/expires_at 等(后端加密存储,不在响应中返回)"
),
)
name: str | None = Field(default=None, min_length=1, max_length=100, description="密钥名称")
rate_multipliers: dict[str, float] | None = Field(
default=None,
description="按 endpoint signature 的成本倍率,如 {'claude:cli': 1.0, 'openai:cli': 0.8}",
)
internal_priority: int | None = Field(
default=None, description="Key 内部优先级(提供商优先模式,数字越小越优先)"
)
global_priority_by_format: dict[str, int] | None = Field(
default=None,
description="按 endpoint signature 的全局优先级,如 {'claude:chat': 1, 'claude:cli': 2}",
)
# rpm_limit: 使用特殊标记区分"未提供"和"设置为 null自适应模式"
# - 不提供字段:不更新
# - 提供 null切换为自适应模式
# - 提供数字:设置固定 RPM 限制
rpm_limit: int | None = Field(
default=None, ge=1, le=10000, description="RPM 限制null=自适应模式)"
)
allowed_models: list[str] | None = Field(
default=None,
description="允许使用的模型列表null=不限制)",
)
capabilities: dict[str, bool] | None = Field(
default=None,
description="Key 能力标签,如 {'cache_1h': true, 'context_1m': true}",
)
cache_ttl_minutes: int | None = Field(
default=None, ge=0, le=60, description="缓存 TTL分钟0=禁用"
)
max_probe_interval_minutes: int | None = Field(
default=None, ge=0, le=32, description="熔断探测间隔(分钟),范围 0-32"
)
is_active: bool | None = Field(default=None, description="是否启用")
note: str | None = Field(default=None, max_length=500, description="备注说明")
auto_fetch_models: bool | None = Field(default=None, description="是否启用自动获取模型")
locked_models: list[str] | None = Field(
default=None, description="被锁定的模型列表(刷新时不会被删除)"
)
# 模型过滤规则(仅当 auto_fetch_models=True 时生效)
model_include_patterns: list[str] | None = Field(
default=None, description="模型包含规则(支持 * 和 ? 通配符),空表示包含所有"
)
model_exclude_patterns: list[str] | None = Field(
default=None, description="模型排除规则(支持 * 和 ? 通配符),空表示不排除"
)
# Key 级别代理配置(覆盖 Provider 级别代理)
# - 不提供:不更新
# - 提供 null清除 Key 级别代理,回退到 Provider 级别代理
# - 提供 ProxyConfig设置 Key 级别代理
proxy: ProxyConfig | None = Field(
default=None,
description="Key 级别代理配置(覆盖 Provider 级别代理null=使用 Provider 级别代理",
)
fingerprint: dict[str, Any] | None = Field(
default=None,
description="请求指纹配置TLS + HTTP 头部)",
)
@field_validator("api_formats")
@classmethod
def validate_api_formats(cls, v: list[str] | None) -> list[str] | None:
"""验证 API 格式列表"""
if v is None:
return v
return EndpointAPIKeyCreate.validate_api_formats(v)
@field_validator("allowed_models")
@classmethod
def validate_allowed_models(cls, v: list[str] | None) -> list[str] | None:
# 与 EndpointAPIKeyCreate 保持一致
return EndpointAPIKeyCreate.validate_allowed_models(v)
@field_validator("api_key")
@classmethod
def validate_api_key(cls, v: str | None) -> str | None:
"""验证 API Key 基本格式"""
if v is None:
return v
return v.strip()
@field_validator("name")
@classmethod
def validate_name(cls, v: str | None) -> str | None:
"""验证名称(防止 XSS"""
if v is None:
return v
v = re.sub(r"<script.*?</script>", "", v, flags=re.IGNORECASE | re.DOTALL)
v = re.sub(r"<iframe.*?</iframe>", "", v, flags=re.IGNORECASE | re.DOTALL)
v = re.sub(r"javascript:", "", v, flags=re.IGNORECASE)
v = re.sub(r"on\w+\s*=", "", v, flags=re.IGNORECASE)
return v.strip()
@field_validator("note")
@classmethod
def validate_note(cls, v: str | None) -> str | None:
"""验证备注(防止 XSS"""
if v is None:
return v
v = re.sub(r"<script.*?</script>", "", v, flags=re.IGNORECASE | re.DOTALL)
v = re.sub(r"<iframe.*?</iframe>", "", v, flags=re.IGNORECASE | re.DOTALL)
v = re.sub(r"javascript:", "", v, flags=re.IGNORECASE)
v = re.sub(r"on\w+\s*=", "", v, flags=re.IGNORECASE)
return v.strip()
class OAuthOrganizationResponse(BaseModel):
id: str | None = Field(default=None, description="OAuth 组织/工作区 ID")
title: str | None = Field(default=None, description="OAuth 组织/工作区标题")
is_default: bool = Field(default=False, description="是否为默认组织/工作区")
role: str | None = Field(default=None, description="当前账号在组织中的角色")
class EndpointAPIKeyResponse(BaseModel):
"""Endpoint API Key 响应"""
id: str
provider_id: str = Field(..., description="Provider ID")
api_formats: list[str] = Field(
default=[],
description="支持的 endpoint signature 列表(如 openai:chat, claude:cli",
)
# Key 信息(脱敏)
api_key_masked: str = Field(..., description="脱敏后的 Key")
api_key_plain: str | None = Field(default=None, description="完整的 Key")
auth_type: str = Field(
default="api_key", description="认证类型api_key / service_account / oauth"
)
# auth_config 不在响应中返回(包含敏感信息),前端通过 auth_type 判断类型
name: str = Field(..., description="密钥名称")
# 成本计算
rate_multipliers: dict[str, float] | None = Field(
default=None,
description="按 endpoint signature 的成本倍率,如 {'claude:cli': 1.0, 'openai:cli': 0.8}",
)
# 优先级和限制
internal_priority: int = Field(default=50, description="Endpoint 内部优先级")
global_priority_by_format: dict[str, int] | None = Field(
default=None, description="按 endpoint signature 的全局优先级"
)
rpm_limit: int | None = None
allowed_models: list[str] | None = None
capabilities: dict[str, bool] | None = Field(default=None, description="Key 能力标签")
# OAuth 相关
oauth_expires_at: int | None = Field(
default=None, description="OAuth Token 过期时间Unix 时间戳)"
)
oauth_email: str | None = Field(default=None, description="OAuth 账号邮箱")
oauth_plan_type: str | None = Field(
default=None, description="OAuth 账号套餐类型(如 free/plus/team/enterprise"
)
oauth_account_id: str | None = Field(default=None, description="OAuth 账号 ID")
oauth_account_name: str | None = Field(default=None, description="OAuth 当前工作区/账号名称")
oauth_account_user_id: str | None = Field(
default=None,
description="OAuth 账号-工作区联合 ID如 Codex chatgpt_account_user_id",
)
oauth_organizations: list[OAuthOrganizationResponse] = Field(
default_factory=list,
description="OAuth 关联的组织/工作区摘要列表",
)
oauth_invalid_at: int | None = Field(
default=None,
description="OAuth Token 失效时间Unix 时间戳,兼容字段;优先使用 status_snapshot.oauth",
)
oauth_invalid_reason: str | None = Field(
default=None, description="OAuth Token 失效原因(兼容字段;优先使用 status_snapshot.oauth"
)
status_snapshot: ProviderKeyStatusSnapshotResponse = Field(
default_factory=ProviderKeyStatusSnapshotResponse,
description="统一的账号/OAuth/额度状态快照",
)
# 缓存与熔断配置
cache_ttl_minutes: int = Field(default=5, description="缓存 TTL分钟0=禁用")
max_probe_interval_minutes: int = Field(default=32, description="熔断探测间隔(分钟)")
# 按 endpoint signature 的健康度数据
health_by_format: dict[str, Any] | None = Field(
default=None, description="按 endpoint signature 存储的健康度数据"
)
circuit_breaker_by_format: dict[str, Any] | None = Field(
default=None, description="按 endpoint signature 存储的熔断器状态"
)
# 聚合字段(从 health_by_format 计算,用于列表显示)
health_score: float = Field(default=1.0, description="健康度(所有格式中的最低值)")
consecutive_failures: int = Field(default=0, description="连续失败次数")
last_failure_at: datetime | None = None
# 聚合熔断器字段
circuit_breaker_open: bool = Field(default=False, description="熔断器是否打开(任何格式)")
circuit_breaker_open_at: datetime | None = Field(default=None, description="熔断器打开时间")
next_probe_at: datetime | None = Field(default=None, description="下次进入半开状态时间")
half_open_until: datetime | None = Field(default=None, description="半开状态结束时间")
half_open_successes: int | None = Field(default=0, description="半开状态成功次数")
half_open_failures: int | None = Field(default=0, description="半开状态失败次数")
request_results_window: list[dict[str, Any]] | None = Field(
None, description="请求结果滑动窗口"
)
# 使用统计
request_count: int
success_count: int
error_count: int
success_rate: float = Field(default=0.0, description="成功率")
avg_response_time_ms: float = Field(default=0.0, description="平均响应时间(毫秒)")
# 状态
is_active: bool
# 自适应 RPM 信息
is_adaptive: bool = Field(default=False, description="是否为自适应模式rpm_limit=NULL")
learned_rpm_limit: int | None = Field(None, description="学习到的 RPM 限制")
effective_limit: int | None = Field(None, description="当前有效限制")
# 滑动窗口利用率采样
utilization_samples: list[dict[str, Any]] | None = Field(None, description="利用率采样窗口")
last_probe_increase_at: datetime | None = Field(None, description="上次探测性扩容时间")
concurrent_429_count: int | None = None
rpm_429_count: int | None = None
last_429_at: datetime | None = None
last_429_type: str | None = None
# 备注
note: str | None = None
# 自动获取模型
auto_fetch_models: bool = Field(default=False, description="是否启用自动获取模型")
last_models_fetch_at: datetime | None = Field(None, description="最后获取模型时间")
last_models_fetch_error: str | None = Field(None, description="最后获取模型错误信息")
locked_models: list[str] | None = Field(None, description="被锁定的模型列表")
# 模型过滤规则
model_include_patterns: list[str] | None = Field(None, description="模型包含规则")
model_exclude_patterns: list[str] | None = Field(None, description="模型排除规则")
# 上游元数据(由响应头采集,如 Codex 额度信息)
upstream_metadata: dict[str, Any] | None = Field(
None, description="上游元数据(如 Codex 额度信息)"
)
# Key 级别代理配置
proxy: dict[str, Any] | None = Field(
None, description="Key 级别代理配置(覆盖 Provider 级别代理)"
)
fingerprint: dict[str, Any] | None = Field(
None,
description="请求指纹配置TLS + HTTP 头部)",
)
# 时间戳
last_used_at: datetime | None = None
created_at: datetime
updated_at: datetime
model_config = ConfigDict(from_attributes=True)
# ========== 健康监控相关 ==========
class FormatHealthData(BaseModel):
"""单个 API 格式的健康度数据"""
health_score: float = 1.0
error_rate: float = 0.0
window_size: int = 0
consecutive_failures: int = 0
last_failure_at: str | None = None
circuit_breaker: dict[str, Any] = Field(default_factory=dict)
class HealthStatusResponse(BaseModel):
"""健康状态响应(支持按格式查询)"""
# 基础信息
key_id: str
key_is_active: bool
key_statistics: dict[str, Any] | None = None
# 整体健康度(取所有格式中的最低值)
key_health_score: float = 1.0
any_circuit_open: bool = False
# 按格式的健康度数据
health_by_format: dict[str, FormatHealthData] | None = None
# 单格式查询时的字段
api_format: str | None = None
key_consecutive_failures: int | None = None
key_last_failure_at: str | None = None
# 单格式查询时的熔断器状态
circuit_breaker_open: bool = False
circuit_breaker_open_at: str | None = None
next_probe_at: str | None = None
half_open_until: str | None = None
half_open_successes: int = 0
half_open_failures: int = 0
class HealthSummaryResponse(BaseModel):
"""健康状态摘要"""
endpoints: dict[str, int] = Field(..., description="Endpoint 统计 (total, active, unhealthy)")
keys: dict[str, int] = Field(..., description="Key 统计 (total, active, unhealthy)")
# ========== RPM 控制相关 ==========
class KeyRpmStatusResponse(BaseModel):
"""Key RPM 状态响应"""
key_id: str = Field(..., description="Key ID")
current_rpm: int = Field(default=0, description="当前 RPM 计数")
rpm_limit: int | None = Field(default=None, description="RPM 限制")
class KeyPriorityItem(BaseModel):
"""单个 Key 优先级项"""
key_id: str = Field(..., description="Key ID")
internal_priority: int = Field(..., ge=0, description="Key 内部优先级(数字越小越优先)")
class BatchUpdateKeyPriorityRequest(BaseModel):
"""批量更新 Key 优先级请求"""
priorities: list[KeyPriorityItem] = Field(..., min_length=1, description="Key 优先级列表")
# ========== 提供商摘要(增强版) ==========
class ProviderUpdateRequest(BaseModel):
"""Provider 基础配置更新请求"""
name: str | None = Field(None, min_length=1, max_length=100)
description: str | None = None
website: str | None = Field(None, max_length=500, description="主站网站")
provider_priority: int | None = Field(None, description="提供商优先级(数字越小越优先)")
keep_priority_on_conversion: bool | None = Field(
None,
description="格式转换时是否保持优先级True=保持原优先级False=需要转换时降级)",
)
enable_format_conversion: bool | None = Field(
None,
description="是否允许格式转换(提供商级别开关)",
)
is_active: bool | None = None
billing_type: str | None = Field(
None, description="计费类型monthly_quota/pay_as_you_go/free_tier"
)
monthly_quota_usd: float | None = Field(None, ge=0, description="订阅配额(美元)")
quota_reset_day: int | None = Field(None, ge=1, le=31, description="配额重置日1-31")
quota_expires_at: datetime | None = Field(None, description="配额过期时间")
# 请求配置(从 Endpoint 迁移)
max_retries: int | None = Field(None, ge=0, le=10, description="最大重试次数")
proxy: dict[str, Any] | None = Field(None, description="代理配置")
# 超时配置(秒),为空时使用全局配置
stream_first_byte_timeout: float | None = Field(
None, ge=1, le=300, description="流式请求首字节超时(秒)"
)
request_timeout: float | None = Field(
None, ge=1, le=600, description="非流式请求整体超时(秒)"
)
claude_code_advanced: ClaudeCodeAdvancedConfig | None = Field(
None, description="Claude Code 高级配置"
)
pool_advanced: PoolAdvancedConfig | None = Field(None, description="通用号池配置")
failover_rules: FailoverRulesConfig | None = Field(None, description="故障转移规则配置")
class ProviderWithEndpointsSummary(BaseModel):
"""Provider 和 Endpoints 摘要"""
# Provider 基本信息
id: str
name: str
provider_type: str | None = Field(
default=None,
description="Provider 类型custom/claude_code/codex/gemini_cli/antigravity",
)
description: str | None = None
website: str | None = None
provider_priority: int = Field(default=100, description="提供商优先级(数字越小越优先)")
keep_priority_on_conversion: bool = Field(
default=False,
description="格式转换时是否保持优先级True=保持原优先级False=需要转换时降级)",
)
enable_format_conversion: bool = Field(
default=True,
description="是否允许格式转换(提供商级别开关)",
)
is_active: bool
# 计费相关字段
billing_type: str | None = None
monthly_quota_usd: float | None = None
monthly_used_usd: float | None = None
quota_reset_day: int | None = Field(default=None, description="配额重置周期(天数)")
quota_last_reset_at: datetime | None = Field(default=None, description="当前周期开始时间")
quota_expires_at: datetime | None = Field(default=None, description="配额过期时间")
# 请求配置(从 Endpoint 迁移)
max_retries: int | None = Field(default=2, description="最大重试次数")
proxy: dict[str, Any] | None = Field(default=None, description="代理配置")
# 超时配置(秒),为空时使用全局配置
stream_first_byte_timeout: float | None = Field(
default=None, description="流式请求首字节超时(秒)"
)
request_timeout: float | None = Field(default=None, description="非流式请求整体超时(秒)")
claude_code_advanced: ClaudeCodeAdvancedConfig | None = Field(
default=None, description="Claude Code 高级配置"
)
pool_advanced: PoolAdvancedConfig | None = Field(default=None, description="通用号池配置")
failover_rules: FailoverRulesConfig | None = Field(default=None, description="故障转移规则配置")
# Endpoint 统计
total_endpoints: int = Field(default=0, description="总 Endpoint 数量")
active_endpoints: int = Field(default=0, description="活跃 Endpoint 数量")
# Key 统计(所有 Endpoints 的 Keys
total_keys: int = Field(default=0, description="总 Key 数量")
active_keys: int = Field(default=0, description="活跃 Key 数量")
# Model 统计
total_models: int = Field(default=0, description="总模型数量")
active_models: int = Field(default=0, description="活跃模型数量")
global_model_ids: list[str] = Field(default=[], description="活跃模型关联的全局模型 ID 列表")
# API 格式列表
api_formats: list[str] = Field(default=[], description="支持的 API 格式列表")
# Endpoint 健康度详情
endpoint_health_details: list[dict[str, Any]] = Field(
default=[],
description="每个 Endpoint 的健康度详情 [{api_format: str, health_score: float, is_active: bool}]",
)
# 健康度统计
avg_health_score: float = Field(default=1.0, description="平均健康度")
unhealthy_endpoints: int = Field(
default=0, description="不健康的端点数量health_score < 0.5"
)
# Provider Ops 配置状态
ops_configured: bool = Field(default=False, description="是否配置了扩展操作(余额监控等)")
ops_architecture_id: str | None = Field(
default=None, description="扩展操作使用的架构 ID如 cubence, anyrouter"
)
# 时间戳
created_at: datetime
updated_at: datetime
model_config = ConfigDict(from_attributes=True)
class ProviderSummaryPageResponse(BaseModel):
"""Provider 摘要分页响应"""
total: int
page: int
page_size: int
items: list[ProviderWithEndpointsSummary]
# ========== 健康监控可视化模型 ==========
class EndpointHealthEvent(BaseModel):
"""单个端点的请求事件"""
timestamp: datetime
status: str
status_code: int | None = None
latency_ms: int | None = None
error_type: str | None = None
error_message: str | None = None
class EndpointHealthMonitor(BaseModel):
"""端点健康监控信息"""
endpoint_id: str
api_format: str
is_active: bool
total_attempts: int
success_count: int
failed_count: int
skipped_count: int
success_rate: float = Field(default=1.0, description="最近事件窗口的成功率")
last_event_at: datetime | None = None
events: list[EndpointHealthEvent] = Field(default_factory=list)
class ProviderEndpointHealthMonitorResponse(BaseModel):
"""Provider 下所有端点的健康监控"""
provider_id: str
provider_name: str
generated_at: datetime
endpoints: list[EndpointHealthMonitor] = Field(default_factory=list)
class ApiFormatHealthMonitor(BaseModel):
"""按 API 格式聚合的健康监控信息"""
api_format: str
total_attempts: int
success_count: int
failed_count: int
skipped_count: int
success_rate: float = Field(default=1.0, description="最近事件窗口的成功率")
provider_count: int = Field(default=0, description="参与统计的 Provider 数量")
key_count: int = Field(default=0, description="参与统计的 API Key 数量")
last_event_at: datetime | None = None
events: list[EndpointHealthEvent] = Field(default_factory=list)
timeline: list[str] = Field(
default_factory=list,
description="Usage 表生成的健康时间线healthy/warning/unhealthy/unknown",
)
time_range_start: datetime | None = Field(
default=None, description="时间线所覆盖区间的开始时间"
)
time_range_end: datetime | None = Field(default=None, description="时间线所覆盖区间的结束时间")
class ApiFormatHealthMonitorResponse(BaseModel):
"""所有 API 格式的健康监控汇总"""
generated_at: datetime
formats: list[ApiFormatHealthMonitor] = Field(default_factory=list)
# ========== 公开健康监控模型(不含敏感信息) ==========
class PublicHealthEvent(BaseModel):
"""公开版单个请求事件(不含敏感信息如 provider_id、key_id"""
timestamp: datetime
status: str
status_code: int | None = None
latency_ms: int | None = None
error_type: str | None = None
class PublicApiFormatHealthMonitor(BaseModel):
"""公开版 API 格式健康监控信息(不含敏感信息)"""
api_format: str
api_path: str = Field(default="/", description="该 API 格式的本站请求路径")
total_attempts: int = Field(default=0, description="总请求次数")
success_count: int = Field(default=0, description="成功次数")
failed_count: int = Field(default=0, description="失败次数")
skipped_count: int = Field(default=0, description="跳过次数")
success_rate: float = Field(default=1.0, description="成功率")
last_event_at: datetime | None = None
events: list[PublicHealthEvent] = Field(default_factory=list, description="事件列表")
timeline: list[str] = Field(
default_factory=list,
description="Usage 表生成的健康时间线healthy/warning/unhealthy/unknown",
)
time_range_start: datetime | None = Field(default=None, description="时间线覆盖区间开始时间")
time_range_end: datetime | None = Field(default=None, description="时间线覆盖区间结束时间")
class PublicApiFormatHealthMonitorResponse(BaseModel):
"""公开版健康监控汇总(不含敏感信息)"""
generated_at: datetime
formats: list[PublicApiFormatHealthMonitor] = Field(default_factory=list)