mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/ - 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层 - 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构 - 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations) - 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image - 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
1279 lines
49 KiB
Python
1279 lines
49 KiB
Python
"""
|
||
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 的 action(set/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_account(GCP Service Account)/ oauth(OAuth access_token)",
|
||
)
|
||
auth_config: dict[str, Any] | None = Field(
|
||
default=None,
|
||
description=(
|
||
"认证配置(JSON):service_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_account(GCP Service Account)/ oauth(OAuth access_token)",
|
||
)
|
||
auth_config: dict[str, Any] | None = Field(
|
||
default=None,
|
||
description=(
|
||
"认证配置(JSON):service_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)
|