2025-12-10 20:52:44 +08:00
|
|
|
|
"""
|
|
|
|
|
|
ProviderEndpoint CRUD 管理 API
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2026-01-30 14:30:57 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
import uuid
|
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
from datetime import datetime, timezone
|
2026-02-01 17:28:00 +08:00
|
|
|
|
from typing import Any
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, Query, Request
|
2026-01-30 03:10:21 +08:00
|
|
|
|
from sqlalchemy import and_
|
2025-12-10 20:52:44 +08:00
|
|
|
|
from sqlalchemy.orm import Session
|
2026-01-16 18:24:52 +08:00
|
|
|
|
from sqlalchemy.orm.attributes import flag_modified
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
from src.api.base.admin_adapter import AdminApiAdapter
|
2026-02-01 17:28:00 +08:00
|
|
|
|
from src.api.base.context import ApiRequestContext
|
2026-01-16 19:42:35 +08:00
|
|
|
|
from src.api.base.models_service import invalidate_models_list_cache
|
2026-03-14 11:59:07 +08:00
|
|
|
|
from src.api.base.pipeline import get_pipeline
|
2026-03-02 22:08:39 +08:00
|
|
|
|
from src.core.api_format.metadata import get_default_body_rules_for_endpoint
|
2026-02-01 17:28:00 +08:00
|
|
|
|
from src.core.api_format.signature import parse_signature_key
|
2025-12-10 20:52:44 +08:00
|
|
|
|
from src.core.exceptions import InvalidRequestException, NotFoundException
|
|
|
|
|
|
from src.core.logger import logger
|
2026-03-01 23:32:48 +08:00
|
|
|
|
from src.core.provider_templates.fixed_providers import FIXED_PROVIDERS
|
2026-02-06 16:37:06 +08:00
|
|
|
|
from src.core.provider_types import ProviderType
|
2025-12-10 20:52:44 +08:00
|
|
|
|
from src.database import get_db
|
|
|
|
|
|
from src.models.database import Provider, ProviderAPIKey, ProviderEndpoint
|
|
|
|
|
|
from src.models.endpoint_models import (
|
|
|
|
|
|
ProviderEndpointCreate,
|
|
|
|
|
|
ProviderEndpointResponse,
|
|
|
|
|
|
ProviderEndpointUpdate,
|
|
|
|
|
|
)
|
2026-03-01 23:32:48 +08:00
|
|
|
|
from src.services.provider.stream_policy import UpstreamStreamPolicy, parse_upstream_stream_policy
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
router = APIRouter(tags=["Endpoint Management"])
|
2026-03-14 11:59:07 +08:00
|
|
|
|
pipeline = get_pipeline()
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def mask_proxy_password(proxy_config: dict | None) -> dict | None:
|
2025-12-18 16:14:37 +08:00
|
|
|
|
"""对代理配置中的密码进行脱敏处理"""
|
|
|
|
|
|
if not proxy_config:
|
|
|
|
|
|
return None
|
|
|
|
|
|
masked = dict(proxy_config)
|
|
|
|
|
|
if masked.get("password"):
|
|
|
|
|
|
masked["password"] = "***"
|
|
|
|
|
|
return masked
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-01 23:32:48 +08:00
|
|
|
|
def _is_fixed_provider(provider_type: str | None) -> bool:
|
|
|
|
|
|
"""Whether this provider_type is managed by fixed-provider templates."""
|
|
|
|
|
|
normalized = (provider_type or "custom").strip().lower()
|
|
|
|
|
|
if normalized == ProviderType.CUSTOM.value:
|
|
|
|
|
|
return False
|
|
|
|
|
|
try:
|
|
|
|
|
|
return ProviderType(normalized) in FIXED_PROVIDERS
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
@router.get("/providers/{provider_id}/endpoints", response_model=list[ProviderEndpointResponse])
|
2025-12-10 20:52:44 +08:00
|
|
|
|
async def list_provider_endpoints(
|
|
|
|
|
|
provider_id: str,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
skip: int = Query(0, ge=0, description="跳过的记录数"),
|
|
|
|
|
|
limit: int = Query(100, ge=1, le=1000, description="返回的最大记录数"),
|
|
|
|
|
|
db: Session = Depends(get_db),
|
2026-01-30 03:10:21 +08:00
|
|
|
|
) -> list[ProviderEndpointResponse]:
|
2026-01-07 14:55:07 +08:00
|
|
|
|
"""
|
|
|
|
|
|
获取指定 Provider 的所有 Endpoints
|
|
|
|
|
|
|
|
|
|
|
|
获取指定 Provider 下的所有 Endpoint 列表,包括配置、统计信息等。
|
|
|
|
|
|
结果按创建时间倒序排列。
|
|
|
|
|
|
|
|
|
|
|
|
**路径参数**:
|
|
|
|
|
|
- `provider_id`: Provider ID
|
|
|
|
|
|
|
|
|
|
|
|
**查询参数**:
|
|
|
|
|
|
- `skip`: 跳过的记录数,用于分页(默认 0)
|
|
|
|
|
|
- `limit`: 返回的最大记录数(1-1000,默认 100)
|
|
|
|
|
|
|
|
|
|
|
|
**返回字段**:
|
|
|
|
|
|
- `id`: Endpoint ID
|
|
|
|
|
|
- `provider_id`: Provider ID
|
|
|
|
|
|
- `provider_name`: Provider 名称
|
|
|
|
|
|
- `api_format`: API 格式
|
|
|
|
|
|
- `base_url`: 基础 URL
|
|
|
|
|
|
- `custom_path`: 自定义路径
|
|
|
|
|
|
- `max_retries`: 最大重试次数
|
|
|
|
|
|
- `is_active`: 是否活跃
|
|
|
|
|
|
- `total_keys`: Key 总数
|
|
|
|
|
|
- `active_keys`: 活跃 Key 数量
|
|
|
|
|
|
- `proxy`: 代理配置(密码已脱敏)
|
|
|
|
|
|
- 其他配置字段
|
|
|
|
|
|
"""
|
2025-12-10 20:52:44 +08:00
|
|
|
|
adapter = AdminListProviderEndpointsAdapter(
|
|
|
|
|
|
provider_id=provider_id,
|
|
|
|
|
|
skip=skip,
|
|
|
|
|
|
limit=limit,
|
|
|
|
|
|
)
|
|
|
|
|
|
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/providers/{provider_id}/endpoints", response_model=ProviderEndpointResponse)
|
|
|
|
|
|
async def create_provider_endpoint(
|
|
|
|
|
|
provider_id: str,
|
|
|
|
|
|
endpoint_data: ProviderEndpointCreate,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
|
) -> ProviderEndpointResponse:
|
2026-01-07 14:55:07 +08:00
|
|
|
|
"""
|
|
|
|
|
|
为 Provider 创建新的 Endpoint
|
|
|
|
|
|
|
|
|
|
|
|
为指定 Provider 创建新的 Endpoint,每个 Provider 的每种 API 格式
|
|
|
|
|
|
只能创建一个 Endpoint。
|
|
|
|
|
|
|
|
|
|
|
|
**路径参数**:
|
|
|
|
|
|
- `provider_id`: Provider ID
|
|
|
|
|
|
|
|
|
|
|
|
**请求体字段**:
|
|
|
|
|
|
- `provider_id`: Provider ID(必须与路径参数一致)
|
|
|
|
|
|
- `api_format`: API 格式(如 claude、openai、gemini 等)
|
|
|
|
|
|
- `base_url`: 基础 URL
|
|
|
|
|
|
- `custom_path`: 自定义路径(可选)
|
2026-01-16 01:18:54 +08:00
|
|
|
|
- `header_rules`: 请求头规则列表(可选,支持 set/drop/rename 操作)
|
2026-01-07 14:55:07 +08:00
|
|
|
|
- `max_retries`: 最大重试次数(默认 2)
|
|
|
|
|
|
- `config`: 额外配置(可选)
|
|
|
|
|
|
- `proxy`: 代理配置(可选)
|
|
|
|
|
|
|
|
|
|
|
|
**返回字段**:
|
|
|
|
|
|
- 包含完整的 Endpoint 信息
|
|
|
|
|
|
"""
|
2025-12-10 20:52:44 +08:00
|
|
|
|
adapter = AdminCreateProviderEndpointAdapter(
|
|
|
|
|
|
provider_id=provider_id,
|
|
|
|
|
|
endpoint_data=endpoint_data,
|
|
|
|
|
|
)
|
|
|
|
|
|
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-02 22:08:39 +08:00
|
|
|
|
@router.get("/defaults/{api_format}/body-rules")
|
|
|
|
|
|
async def get_default_endpoint_body_rules(
|
|
|
|
|
|
api_format: str,
|
|
|
|
|
|
request: Request,
|
2026-03-05 19:04:21 +08:00
|
|
|
|
provider_type: str | None = None,
|
2026-03-02 22:08:39 +08:00
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""获取指定 endpoint signature 的默认 body_rules。"""
|
2026-03-05 19:04:21 +08:00
|
|
|
|
adapter = AdminGetDefaultBodyRulesAdapter(
|
|
|
|
|
|
api_format=api_format, provider_type=provider_type or None
|
|
|
|
|
|
)
|
2026-03-02 22:08:39 +08:00
|
|
|
|
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
@router.get("/{endpoint_id}", response_model=ProviderEndpointResponse)
|
|
|
|
|
|
async def get_endpoint(
|
|
|
|
|
|
endpoint_id: str,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
|
) -> ProviderEndpointResponse:
|
2026-01-07 14:55:07 +08:00
|
|
|
|
"""
|
|
|
|
|
|
获取 Endpoint 详情
|
|
|
|
|
|
|
|
|
|
|
|
获取指定 Endpoint 的详细信息,包括配置、统计信息等。
|
|
|
|
|
|
|
|
|
|
|
|
**路径参数**:
|
|
|
|
|
|
- `endpoint_id`: Endpoint ID
|
|
|
|
|
|
|
|
|
|
|
|
**返回字段**:
|
|
|
|
|
|
- `id`: Endpoint ID
|
|
|
|
|
|
- `provider_id`: Provider ID
|
|
|
|
|
|
- `provider_name`: Provider 名称
|
|
|
|
|
|
- `api_format`: API 格式
|
|
|
|
|
|
- `base_url`: 基础 URL
|
|
|
|
|
|
- `custom_path`: 自定义路径
|
|
|
|
|
|
- `max_retries`: 最大重试次数
|
|
|
|
|
|
- `is_active`: 是否活跃
|
|
|
|
|
|
- `total_keys`: Key 总数
|
|
|
|
|
|
- `active_keys`: 活跃 Key 数量
|
|
|
|
|
|
- `proxy`: 代理配置(密码已脱敏)
|
|
|
|
|
|
- 其他配置字段
|
|
|
|
|
|
"""
|
2025-12-10 20:52:44 +08:00
|
|
|
|
adapter = AdminGetProviderEndpointAdapter(endpoint_id=endpoint_id)
|
|
|
|
|
|
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/{endpoint_id}", response_model=ProviderEndpointResponse)
|
|
|
|
|
|
async def update_endpoint(
|
|
|
|
|
|
endpoint_id: str,
|
|
|
|
|
|
endpoint_data: ProviderEndpointUpdate,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
|
) -> ProviderEndpointResponse:
|
2026-01-07 14:55:07 +08:00
|
|
|
|
"""
|
|
|
|
|
|
更新 Endpoint
|
|
|
|
|
|
|
|
|
|
|
|
更新指定 Endpoint 的配置。支持部分更新。
|
|
|
|
|
|
|
|
|
|
|
|
**路径参数**:
|
|
|
|
|
|
- `endpoint_id`: Endpoint ID
|
|
|
|
|
|
|
|
|
|
|
|
**请求体字段**(均为可选):
|
|
|
|
|
|
- `base_url`: 基础 URL
|
|
|
|
|
|
- `custom_path`: 自定义路径
|
2026-01-16 01:18:54 +08:00
|
|
|
|
- `header_rules`: 请求头规则列表
|
2026-01-07 14:55:07 +08:00
|
|
|
|
- `max_retries`: 最大重试次数
|
|
|
|
|
|
- `is_active`: 是否活跃
|
|
|
|
|
|
- `config`: 额外配置
|
|
|
|
|
|
- `proxy`: 代理配置(设置为 null 可清除代理)
|
|
|
|
|
|
|
|
|
|
|
|
**返回字段**:
|
|
|
|
|
|
- 包含更新后的完整 Endpoint 信息
|
|
|
|
|
|
"""
|
2025-12-10 20:52:44 +08:00
|
|
|
|
adapter = AdminUpdateProviderEndpointAdapter(
|
|
|
|
|
|
endpoint_id=endpoint_id,
|
|
|
|
|
|
endpoint_data=endpoint_data,
|
|
|
|
|
|
)
|
|
|
|
|
|
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/{endpoint_id}")
|
|
|
|
|
|
async def delete_endpoint(
|
|
|
|
|
|
endpoint_id: str,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
|
) -> dict:
|
2026-01-07 14:55:07 +08:00
|
|
|
|
"""
|
|
|
|
|
|
删除 Endpoint
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
删除指定的 Endpoint,会影响该 Provider 在该 API 格式下的路由能力。
|
|
|
|
|
|
Key 不会被删除,但包含该 API 格式的 Key 将无法被调度使用(直到重新创建该格式的 Endpoint)。
|
2026-01-07 14:55:07 +08:00
|
|
|
|
|
|
|
|
|
|
**路径参数**:
|
|
|
|
|
|
- `endpoint_id`: Endpoint ID
|
|
|
|
|
|
|
|
|
|
|
|
**返回字段**:
|
|
|
|
|
|
- `message`: 操作结果消息
|
2026-01-10 18:43:53 +08:00
|
|
|
|
- `affected_keys_count`: 受影响的 Key 数量(包含该 API 格式)
|
2026-01-07 14:55:07 +08:00
|
|
|
|
"""
|
2025-12-10 20:52:44 +08:00
|
|
|
|
adapter = AdminDeleteProviderEndpointAdapter(endpoint_id=endpoint_id)
|
|
|
|
|
|
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# -------- Adapters --------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class AdminListProviderEndpointsAdapter(AdminApiAdapter):
|
|
|
|
|
|
provider_id: str
|
|
|
|
|
|
skip: int
|
|
|
|
|
|
limit: int
|
|
|
|
|
|
|
2026-01-30 14:30:57 +08:00
|
|
|
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
2025-12-10 20:52:44 +08:00
|
|
|
|
db = context.db
|
|
|
|
|
|
provider = db.query(Provider).filter(Provider.id == self.provider_id).first()
|
|
|
|
|
|
if not provider:
|
|
|
|
|
|
raise NotFoundException(f"Provider {self.provider_id} 不存在")
|
|
|
|
|
|
|
|
|
|
|
|
endpoints = (
|
|
|
|
|
|
db.query(ProviderEndpoint)
|
|
|
|
|
|
.filter(ProviderEndpoint.provider_id == self.provider_id)
|
|
|
|
|
|
.order_by(ProviderEndpoint.created_at.desc())
|
|
|
|
|
|
.offset(self.skip)
|
|
|
|
|
|
.limit(self.limit)
|
|
|
|
|
|
.all()
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
# Key 是 Provider 级别资源:按 key.api_formats 归类到各 Endpoint.api_format 下
|
|
|
|
|
|
keys = (
|
|
|
|
|
|
db.query(ProviderAPIKey.api_formats, ProviderAPIKey.is_active)
|
|
|
|
|
|
.filter(ProviderAPIKey.provider_id == self.provider_id)
|
|
|
|
|
|
.all()
|
|
|
|
|
|
)
|
|
|
|
|
|
total_keys_map: dict[str, int] = {}
|
|
|
|
|
|
active_keys_map: dict[str, int] = {}
|
|
|
|
|
|
for api_formats, is_active in keys:
|
2026-02-01 17:28:00 +08:00
|
|
|
|
for fmt in api_formats or []:
|
2026-01-10 18:43:53 +08:00
|
|
|
|
total_keys_map[fmt] = total_keys_map.get(fmt, 0) + 1
|
|
|
|
|
|
if is_active:
|
|
|
|
|
|
active_keys_map[fmt] = active_keys_map.get(fmt, 0) + 1
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
result: list[ProviderEndpointResponse] = []
|
2025-12-10 20:52:44 +08:00
|
|
|
|
for endpoint in endpoints:
|
2026-01-10 18:43:53 +08:00
|
|
|
|
endpoint_format = (
|
|
|
|
|
|
endpoint.api_format
|
|
|
|
|
|
if isinstance(endpoint.api_format, str)
|
|
|
|
|
|
else endpoint.api_format.value
|
|
|
|
|
|
)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
endpoint_dict = {
|
|
|
|
|
|
**endpoint.__dict__,
|
|
|
|
|
|
"provider_name": provider.name,
|
|
|
|
|
|
"api_format": endpoint.api_format,
|
2026-01-10 18:43:53 +08:00
|
|
|
|
"total_keys": total_keys_map.get(endpoint_format, 0),
|
|
|
|
|
|
"active_keys": active_keys_map.get(endpoint_format, 0),
|
2025-12-18 16:14:37 +08:00
|
|
|
|
"proxy": mask_proxy_password(endpoint.proxy),
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
endpoint_dict.pop("_sa_instance_state", None)
|
|
|
|
|
|
result.append(ProviderEndpointResponse(**endpoint_dict))
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class AdminCreateProviderEndpointAdapter(AdminApiAdapter):
|
|
|
|
|
|
provider_id: str
|
|
|
|
|
|
endpoint_data: ProviderEndpointCreate
|
|
|
|
|
|
|
2026-01-30 14:30:57 +08:00
|
|
|
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
2025-12-10 20:52:44 +08:00
|
|
|
|
db = context.db
|
|
|
|
|
|
provider = db.query(Provider).filter(Provider.id == self.provider_id).first()
|
|
|
|
|
|
if not provider:
|
|
|
|
|
|
raise NotFoundException(f"Provider {self.provider_id} 不存在")
|
|
|
|
|
|
|
2026-02-04 10:24:25 +08:00
|
|
|
|
# 固定类型 Provider:禁止通过该接口新增 Endpoints(端点由模板自动创建并锁定)
|
2026-03-05 19:04:21 +08:00
|
|
|
|
provider_type = getattr(provider, "provider_type", None) or "custom"
|
2026-03-01 23:32:48 +08:00
|
|
|
|
if _is_fixed_provider(provider_type):
|
2026-02-04 10:24:25 +08:00
|
|
|
|
raise InvalidRequestException("固定类型 Provider 不允许手动新增 Endpoint")
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
if self.endpoint_data.provider_id != self.provider_id:
|
|
|
|
|
|
raise InvalidRequestException("provider_id 不匹配")
|
|
|
|
|
|
|
|
|
|
|
|
existing = (
|
|
|
|
|
|
db.query(ProviderEndpoint)
|
|
|
|
|
|
.filter(
|
|
|
|
|
|
and_(
|
|
|
|
|
|
ProviderEndpoint.provider_id == self.provider_id,
|
|
|
|
|
|
ProviderEndpoint.api_format == self.endpoint_data.api_format,
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
.first()
|
|
|
|
|
|
)
|
|
|
|
|
|
if existing:
|
|
|
|
|
|
raise InvalidRequestException(
|
|
|
|
|
|
f"Provider {provider.name} 已存在 {self.endpoint_data.api_format} 格式的 Endpoint"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
now = datetime.now(timezone.utc)
|
2026-02-01 17:28:00 +08:00
|
|
|
|
sig = parse_signature_key(self.endpoint_data.api_format)
|
|
|
|
|
|
api_family = sig.api_family.value
|
|
|
|
|
|
endpoint_kind = sig.endpoint_kind.value
|
|
|
|
|
|
# 使用归一化后的 signature key,确保格式一致性
|
|
|
|
|
|
normalized_api_format = sig.key
|
2026-03-02 22:08:39 +08:00
|
|
|
|
body_rules = self.endpoint_data.body_rules
|
|
|
|
|
|
if body_rules is None:
|
2026-03-05 19:04:21 +08:00
|
|
|
|
body_rules = (
|
|
|
|
|
|
get_default_body_rules_for_endpoint(
|
|
|
|
|
|
normalized_api_format, provider_type=provider_type
|
|
|
|
|
|
)
|
|
|
|
|
|
or None
|
|
|
|
|
|
)
|
2026-01-16 01:18:54 +08:00
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
new_endpoint = ProviderEndpoint(
|
|
|
|
|
|
id=str(uuid.uuid4()),
|
|
|
|
|
|
provider_id=self.provider_id,
|
2026-02-01 17:28:00 +08:00
|
|
|
|
api_format=normalized_api_format,
|
|
|
|
|
|
api_family=api_family,
|
|
|
|
|
|
endpoint_kind=endpoint_kind,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
base_url=self.endpoint_data.base_url,
|
2026-01-05 09:22:20 +08:00
|
|
|
|
custom_path=self.endpoint_data.custom_path,
|
2026-01-16 01:18:54 +08:00
|
|
|
|
header_rules=self.endpoint_data.header_rules,
|
2026-03-02 22:08:39 +08:00
|
|
|
|
body_rules=body_rules,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
max_retries=self.endpoint_data.max_retries,
|
|
|
|
|
|
is_active=True,
|
|
|
|
|
|
config=self.endpoint_data.config,
|
2025-12-18 14:42:06 +08:00
|
|
|
|
proxy=self.endpoint_data.proxy.model_dump() if self.endpoint_data.proxy else None,
|
2026-01-22 01:48:56 +08:00
|
|
|
|
format_acceptance_config=self.endpoint_data.format_acceptance_config,
|
2025-12-10 20:52:44 +08:00
|
|
|
|
created_at=now,
|
|
|
|
|
|
updated_at=now,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
db.add(new_endpoint)
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
db.refresh(new_endpoint)
|
|
|
|
|
|
|
2026-01-16 19:42:35 +08:00
|
|
|
|
# 清除 /v1/models 列表缓存
|
|
|
|
|
|
await invalidate_models_list_cache()
|
|
|
|
|
|
|
2026-02-01 17:28:00 +08:00
|
|
|
|
logger.info(
|
|
|
|
|
|
f"[OK] 创建 Endpoint: Provider={provider.name}, Format={self.endpoint_data.api_format}, ID={new_endpoint.id}"
|
|
|
|
|
|
)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
endpoint_dict = {
|
|
|
|
|
|
k: v
|
|
|
|
|
|
for k, v in new_endpoint.__dict__.items()
|
2025-12-18 16:14:37 +08:00
|
|
|
|
if k not in {"api_format", "_sa_instance_state", "proxy"}
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
return ProviderEndpointResponse(
|
|
|
|
|
|
**endpoint_dict,
|
|
|
|
|
|
provider_name=provider.name,
|
|
|
|
|
|
api_format=new_endpoint.api_format,
|
2025-12-18 16:14:37 +08:00
|
|
|
|
proxy=mask_proxy_password(new_endpoint.proxy),
|
2025-12-10 20:52:44 +08:00
|
|
|
|
total_keys=0,
|
|
|
|
|
|
active_keys=0,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class AdminGetProviderEndpointAdapter(AdminApiAdapter):
|
|
|
|
|
|
endpoint_id: str
|
|
|
|
|
|
|
2026-01-30 14:30:57 +08:00
|
|
|
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
2025-12-10 20:52:44 +08:00
|
|
|
|
db = context.db
|
|
|
|
|
|
endpoint = (
|
|
|
|
|
|
db.query(ProviderEndpoint, Provider)
|
|
|
|
|
|
.join(Provider, ProviderEndpoint.provider_id == Provider.id)
|
|
|
|
|
|
.filter(ProviderEndpoint.id == self.endpoint_id)
|
|
|
|
|
|
.first()
|
|
|
|
|
|
)
|
|
|
|
|
|
if not endpoint:
|
|
|
|
|
|
raise NotFoundException(f"Endpoint {self.endpoint_id} 不存在")
|
|
|
|
|
|
|
|
|
|
|
|
endpoint_obj, provider = endpoint
|
2026-01-10 18:43:53 +08:00
|
|
|
|
endpoint_format = (
|
|
|
|
|
|
endpoint_obj.api_format
|
|
|
|
|
|
if isinstance(endpoint_obj.api_format, str)
|
|
|
|
|
|
else endpoint_obj.api_format.value
|
2025-12-10 20:52:44 +08:00
|
|
|
|
)
|
2026-01-10 18:43:53 +08:00
|
|
|
|
keys = (
|
|
|
|
|
|
db.query(ProviderAPIKey.api_formats, ProviderAPIKey.is_active)
|
|
|
|
|
|
.filter(ProviderAPIKey.provider_id == endpoint_obj.provider_id)
|
|
|
|
|
|
.all()
|
2025-12-10 20:52:44 +08:00
|
|
|
|
)
|
2026-01-10 18:43:53 +08:00
|
|
|
|
total_keys = 0
|
|
|
|
|
|
active_keys = 0
|
|
|
|
|
|
for api_formats, is_active in keys:
|
|
|
|
|
|
if endpoint_format in (api_formats or []):
|
|
|
|
|
|
total_keys += 1
|
|
|
|
|
|
if is_active:
|
|
|
|
|
|
active_keys += 1
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
endpoint_dict = {
|
|
|
|
|
|
k: v
|
|
|
|
|
|
for k, v in endpoint_obj.__dict__.items()
|
2025-12-18 16:14:37 +08:00
|
|
|
|
if k not in {"api_format", "_sa_instance_state", "proxy"}
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
return ProviderEndpointResponse(
|
|
|
|
|
|
**endpoint_dict,
|
|
|
|
|
|
provider_name=provider.name,
|
|
|
|
|
|
api_format=endpoint_obj.api_format,
|
2025-12-18 16:14:37 +08:00
|
|
|
|
proxy=mask_proxy_password(endpoint_obj.proxy),
|
2025-12-10 20:52:44 +08:00
|
|
|
|
total_keys=total_keys,
|
|
|
|
|
|
active_keys=active_keys,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class AdminUpdateProviderEndpointAdapter(AdminApiAdapter):
|
|
|
|
|
|
endpoint_id: str
|
|
|
|
|
|
endpoint_data: ProviderEndpointUpdate
|
|
|
|
|
|
|
2026-01-30 14:30:57 +08:00
|
|
|
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
2025-12-10 20:52:44 +08:00
|
|
|
|
db = context.db
|
|
|
|
|
|
endpoint = (
|
|
|
|
|
|
db.query(ProviderEndpoint).filter(ProviderEndpoint.id == self.endpoint_id).first()
|
|
|
|
|
|
)
|
|
|
|
|
|
if not endpoint:
|
|
|
|
|
|
raise NotFoundException(f"Endpoint {self.endpoint_id} 不存在")
|
|
|
|
|
|
|
|
|
|
|
|
update_data = self.endpoint_data.model_dump(exclude_unset=True)
|
2026-01-16 01:18:54 +08:00
|
|
|
|
|
2026-02-04 10:24:25 +08:00
|
|
|
|
# 固定类型 Provider 的 endpoint:锁定 base_url/custom_path(前端禁用仅是 UX,后端必须强校验)
|
|
|
|
|
|
provider = db.query(Provider).filter(Provider.id == endpoint.provider_id).first()
|
|
|
|
|
|
if provider:
|
2026-03-01 23:32:48 +08:00
|
|
|
|
provider_type = getattr(provider, "provider_type", "custom")
|
|
|
|
|
|
if _is_fixed_provider(provider_type):
|
2026-02-04 10:24:25 +08:00
|
|
|
|
if "base_url" in update_data or "custom_path" in update_data:
|
|
|
|
|
|
raise InvalidRequestException(
|
|
|
|
|
|
"固定类型 Provider 的 Endpoint 不允许修改 base_url/custom_path"
|
|
|
|
|
|
)
|
2026-03-01 23:32:48 +08:00
|
|
|
|
normalized_provider_type = str(provider_type or "custom").strip().lower()
|
|
|
|
|
|
endpoint_sig = str(getattr(endpoint, "api_format", "") or "").strip().lower()
|
|
|
|
|
|
if (
|
|
|
|
|
|
normalized_provider_type == ProviderType.CODEX.value
|
|
|
|
|
|
and endpoint_sig == "openai:cli"
|
|
|
|
|
|
):
|
|
|
|
|
|
has_config_in_payload = "config" in update_data
|
|
|
|
|
|
cfg_payload = (
|
|
|
|
|
|
update_data.get("config")
|
|
|
|
|
|
if has_config_in_payload
|
|
|
|
|
|
else getattr(endpoint, "config", None)
|
|
|
|
|
|
)
|
|
|
|
|
|
cfg = dict(cfg_payload) if isinstance(cfg_payload, dict) else {}
|
|
|
|
|
|
requested = (
|
|
|
|
|
|
cfg.get("upstream_stream_policy")
|
|
|
|
|
|
or cfg.get("upstreamStreamPolicy")
|
|
|
|
|
|
or cfg.get("upstream_stream")
|
|
|
|
|
|
)
|
|
|
|
|
|
if (
|
|
|
|
|
|
has_config_in_payload
|
|
|
|
|
|
and requested is not None
|
|
|
|
|
|
and parse_upstream_stream_policy(requested)
|
|
|
|
|
|
!= UpstreamStreamPolicy.FORCE_STREAM
|
|
|
|
|
|
):
|
|
|
|
|
|
raise InvalidRequestException(
|
|
|
|
|
|
"Codex OpenAI CLI 端点固定为强制流式,不允许修改"
|
|
|
|
|
|
)
|
|
|
|
|
|
cfg.pop("upstreamStreamPolicy", None)
|
|
|
|
|
|
cfg.pop("upstream_stream", None)
|
|
|
|
|
|
cfg["upstream_stream_policy"] = "force_stream"
|
|
|
|
|
|
update_data["config"] = cfg
|
2026-02-04 10:24:25 +08:00
|
|
|
|
|
2025-12-18 16:14:37 +08:00
|
|
|
|
# 把 proxy 转换为 dict 存储,支持显式设置为 None 清除代理
|
|
|
|
|
|
if "proxy" in update_data:
|
|
|
|
|
|
if update_data["proxy"] is not None:
|
|
|
|
|
|
new_proxy = dict(update_data["proxy"])
|
|
|
|
|
|
# 只有当密码字段未提供时才保留原密码(空字符串视为显式清除)
|
|
|
|
|
|
if "password" not in new_proxy and endpoint.proxy:
|
|
|
|
|
|
old_password = endpoint.proxy.get("password")
|
|
|
|
|
|
if old_password:
|
|
|
|
|
|
new_proxy["password"] = old_password
|
|
|
|
|
|
update_data["proxy"] = new_proxy
|
|
|
|
|
|
# proxy 为 None 时保留,用于清除代理配置
|
2026-02-06 16:37:06 +08:00
|
|
|
|
|
|
|
|
|
|
# JSON 列需要 flag_modified 以确保 SQLAlchemy 检测到变更
|
|
|
|
|
|
json_fields = {"header_rules", "body_rules", "config", "proxy", "format_acceptance_config"}
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
for field, value in update_data.items():
|
|
|
|
|
|
setattr(endpoint, field, value)
|
2026-02-06 16:37:06 +08:00
|
|
|
|
if field in json_fields:
|
|
|
|
|
|
flag_modified(endpoint, field)
|
2026-02-01 17:28:00 +08:00
|
|
|
|
|
|
|
|
|
|
# Phase 3/4: 自动维护新架构字段,确保新增/历史数据都能被调度器按 family/kind 查询
|
|
|
|
|
|
sig = parse_signature_key(endpoint.api_format)
|
|
|
|
|
|
endpoint.api_family = sig.api_family.value
|
|
|
|
|
|
endpoint.endpoint_kind = sig.endpoint_kind.value
|
2025-12-10 20:52:44 +08:00
|
|
|
|
endpoint.updated_at = datetime.now(timezone.utc)
|
|
|
|
|
|
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
db.refresh(endpoint)
|
|
|
|
|
|
|
2026-01-16 19:42:35 +08:00
|
|
|
|
# 清除 /v1/models 列表缓存(is_active 变更会影响模型可用性)
|
|
|
|
|
|
await invalidate_models_list_cache()
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
provider = db.query(Provider).filter(Provider.id == endpoint.provider_id).first()
|
2026-02-01 17:28:00 +08:00
|
|
|
|
logger.info(
|
|
|
|
|
|
f"[OK] 更新 Endpoint: ID={self.endpoint_id}, Updates={list(update_data.keys())}"
|
|
|
|
|
|
)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
endpoint_format = (
|
2026-02-01 17:28:00 +08:00
|
|
|
|
endpoint.api_format
|
|
|
|
|
|
if isinstance(endpoint.api_format, str)
|
|
|
|
|
|
else endpoint.api_format.value
|
2025-12-10 20:52:44 +08:00
|
|
|
|
)
|
2026-01-10 18:43:53 +08:00
|
|
|
|
keys = (
|
|
|
|
|
|
db.query(ProviderAPIKey.api_formats, ProviderAPIKey.is_active)
|
|
|
|
|
|
.filter(ProviderAPIKey.provider_id == endpoint.provider_id)
|
|
|
|
|
|
.all()
|
2025-12-10 20:52:44 +08:00
|
|
|
|
)
|
2026-01-10 18:43:53 +08:00
|
|
|
|
total_keys = 0
|
|
|
|
|
|
active_keys = 0
|
|
|
|
|
|
for api_formats, is_active in keys:
|
|
|
|
|
|
if endpoint_format in (api_formats or []):
|
|
|
|
|
|
total_keys += 1
|
|
|
|
|
|
if is_active:
|
|
|
|
|
|
active_keys += 1
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
endpoint_dict = {
|
|
|
|
|
|
k: v
|
|
|
|
|
|
for k, v in endpoint.__dict__.items()
|
2025-12-18 16:14:37 +08:00
|
|
|
|
if k not in {"api_format", "_sa_instance_state", "proxy"}
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
return ProviderEndpointResponse(
|
|
|
|
|
|
**endpoint_dict,
|
|
|
|
|
|
provider_name=provider.name if provider else "Unknown",
|
|
|
|
|
|
api_format=endpoint.api_format,
|
2025-12-18 16:14:37 +08:00
|
|
|
|
proxy=mask_proxy_password(endpoint.proxy),
|
2025-12-10 20:52:44 +08:00
|
|
|
|
total_keys=total_keys,
|
|
|
|
|
|
active_keys=active_keys,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class AdminDeleteProviderEndpointAdapter(AdminApiAdapter):
|
|
|
|
|
|
endpoint_id: str
|
|
|
|
|
|
|
2026-01-30 14:30:57 +08:00
|
|
|
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
2025-12-10 20:52:44 +08:00
|
|
|
|
db = context.db
|
|
|
|
|
|
endpoint = (
|
|
|
|
|
|
db.query(ProviderEndpoint).filter(ProviderEndpoint.id == self.endpoint_id).first()
|
|
|
|
|
|
)
|
|
|
|
|
|
if not endpoint:
|
|
|
|
|
|
raise NotFoundException(f"Endpoint {self.endpoint_id} 不存在")
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
endpoint_format = (
|
2026-02-01 17:28:00 +08:00
|
|
|
|
endpoint.api_format
|
|
|
|
|
|
if isinstance(endpoint.api_format, str)
|
|
|
|
|
|
else endpoint.api_format.value
|
2026-01-10 18:43:53 +08:00
|
|
|
|
)
|
2026-01-16 18:24:52 +08:00
|
|
|
|
|
|
|
|
|
|
# 查询包含该格式的所有 Key,并从 api_formats 中移除该格式
|
2026-01-10 18:43:53 +08:00
|
|
|
|
keys = (
|
2026-01-16 18:24:52 +08:00
|
|
|
|
db.query(ProviderAPIKey)
|
2026-01-10 18:43:53 +08:00
|
|
|
|
.filter(ProviderAPIKey.provider_id == endpoint.provider_id)
|
|
|
|
|
|
.all()
|
|
|
|
|
|
)
|
2026-01-16 18:24:52 +08:00
|
|
|
|
affected_keys_count = 0
|
|
|
|
|
|
for key in keys:
|
|
|
|
|
|
if key.api_formats and endpoint_format in key.api_formats:
|
|
|
|
|
|
affected_keys_count += 1
|
|
|
|
|
|
# 移除该格式
|
|
|
|
|
|
new_formats = [f for f in key.api_formats if f != endpoint_format]
|
|
|
|
|
|
key.api_formats = new_formats if new_formats else []
|
2026-02-01 17:28:00 +08:00
|
|
|
|
flag_modified(key, "api_formats")
|
2026-01-16 18:24:52 +08:00
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
db.delete(endpoint)
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
|
2026-01-16 19:42:35 +08:00
|
|
|
|
# 清除 /v1/models 列表缓存
|
|
|
|
|
|
await invalidate_models_list_cache()
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
logger.warning(
|
|
|
|
|
|
f"[DELETE] 删除 Endpoint: ID={self.endpoint_id}, Format={endpoint_format}, "
|
|
|
|
|
|
f"AffectedKeys={affected_keys_count}"
|
|
|
|
|
|
)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
return {
|
|
|
|
|
|
"message": f"Endpoint {self.endpoint_id} 已删除",
|
|
|
|
|
|
"affected_keys_count": affected_keys_count,
|
|
|
|
|
|
}
|
2026-03-02 22:08:39 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
|
class AdminGetDefaultBodyRulesAdapter(AdminApiAdapter):
|
|
|
|
|
|
api_format: str
|
2026-03-05 19:04:21 +08:00
|
|
|
|
provider_type: str | None = None
|
2026-03-02 22:08:39 +08:00
|
|
|
|
|
|
|
|
|
|
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
|
|
|
|
|
try:
|
|
|
|
|
|
normalized_api_format = parse_signature_key(self.api_format).key
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
raise InvalidRequestException(f"无效的 api_format: {self.api_format}") from exc
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"api_format": normalized_api_format,
|
2026-03-05 19:04:21 +08:00
|
|
|
|
"body_rules": get_default_body_rules_for_endpoint(
|
|
|
|
|
|
normalized_api_format, provider_type=self.provider_type
|
|
|
|
|
|
),
|
2026-03-02 22:08:39 +08:00
|
|
|
|
}
|