M4b:ai_provider 删除手术①-⑥——神识全面接管模型出口
- ① 后端删除 app/ai_provider/(38 文件,含 anthropic/openai 格式、billing、audio、embedding)、services/ai_provider_admin.py、core/config/model_config.py、tests/ai_provider/(21 文件) - ② 连带退役 application/agent/(旧 Agent 循环/steer/follow-up/确认注册表)、application/skill_executor.py、app/skills/(python skills 孤儿)、endpoints/audio.py、application/imports.py(ProviderManager 消费方);settings 端点收敛为生成偏好 - ③ runs.py 修剪:AgentRunRegistry/steer/follow-up/confirmations 端点退役,保留 list/get/events/cancel/stream;safety 确认改为直接落库裁决(pending→approved/denied) - ④ 前端删除:models 页、ModelGatewayView、gateway.ts、GatewayPanel(玄关)、ModelGatewayPanel/ModelStrategyForm、audio.ts/useVoiceInput、网关类型与 mock;设置页收敛为外观/语言/通知 - ⑤ 配置收敛:config/models|providers|pricing.example.yaml 归档 shenshi/config/;.env.example 删 provider keys/MODEL_ENDPOINT_POOL/MODEL_CONFIG_PATH,收敛 SHENSHI_* - ⑥ 验收:rg 审计灵台 Provider SDK import 与 ai_provider 引用归零;后端 528 passed(2 个预存失败经 HEAD 复跑验证与本手术无关)、前端 type-check+201 passed、神识 go vet+test、BFF 4 passed
This commit is contained in:
1 parent
0d383725b5
commit
462d570751
129 files changed
+81
-15867
No files matched your search
@@ -1,275 +0,0 @@
|
||||
"""音频相关 API 端点 — TTS(文本转语音)、ASR(语音转文本)等。
|
||||
|
||||
音频端点使用神识的模型注册表查找可用的音频模型,并调用对应音频适配器合成 /
|
||||
转写。ASR 与 TTS 按 ``tags`` 中的 ``asr`` / ``tts`` 能力区分,ASR 内部再按
|
||||
``provider`` 区分协议:``yuanqi`` 走 multipart ``/v1/audio/transcriptions``,
|
||||
其他走 MiMo 风格的 ``/chat/completions`` + ``input_audio``。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, File, Form, HTTPException, Response, UploadFile
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.ai_provider.audio import AudioService, AudioServiceError
|
||||
from app.ai_provider.manager import ProviderManagerFactory
|
||||
from app.core.logging import logger
|
||||
|
||||
router = APIRouter(tags=["audio"])
|
||||
|
||||
_MIME_MAP = {
|
||||
"wav": "audio/wav",
|
||||
"mp3": "audio/mpeg",
|
||||
"opus": "audio/opus",
|
||||
"flac": "audio/flac",
|
||||
}
|
||||
|
||||
|
||||
class _AudioModelFactory:
|
||||
"""Adapter: wraps ProviderManager to provide ModelFactory interface for AudioService.
|
||||
|
||||
``create_audio(name)`` 按 ``tags`` 判断 ``asr`` / ``tts`` 能力:
|
||||
- ``tts`` → ``HunjingTTSOpenAI``(``/chat/completions`` + ``audio``)
|
||||
- ``asr`` → 按 ``provider`` 分派:
|
||||
- ``yuanqi`` → ``YuanqiASRWhisper``(multipart ``/v1/audio/transcriptions``)
|
||||
- 其他 → ``HunjingASROpenAI``(``/chat/completions`` + ``input_audio``)
|
||||
"""
|
||||
|
||||
def __init__(self, manager) -> None:
|
||||
self._manager = manager
|
||||
|
||||
@property
|
||||
def registry(self):
|
||||
return self._manager.model_registry
|
||||
|
||||
def create_audio(self, name: str):
|
||||
model = self._manager.get_model_config(name)
|
||||
is_asr = "asr" in model.tags
|
||||
|
||||
if is_asr:
|
||||
return self._create_asr(model)
|
||||
return self._create_tts(model)
|
||||
|
||||
def _provider_or_none(self, model):
|
||||
"""安全获取 provider 配置(provider 未配置时返回 None,走纯端点解析)。"""
|
||||
try:
|
||||
return self._manager.get_provider_config(model.provider)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# ── ASR ─────────────────────────────────────────────────────────────
|
||||
|
||||
def _create_asr(self, model):
|
||||
"""按 provider 选择 ASR 协议适配器。"""
|
||||
# yuanqi 本地引擎走 multipart /v1/audio/transcriptions
|
||||
if model.provider == "yuanqi":
|
||||
from app.ai_provider.audio.yuanqi_asr import YuanqiASRWhisper
|
||||
|
||||
if model.endpoints:
|
||||
return YuanqiASRWhisper.from_model_config(
|
||||
model, provider=self._provider_or_none(model)
|
||||
)
|
||||
|
||||
provider = self._manager.get_provider_config(model.provider)
|
||||
api_key, base_url = self._resolve_credentials(provider)
|
||||
if not base_url:
|
||||
raise AudioServiceError(
|
||||
status_code=500,
|
||||
code="audio_asr_no_base_url",
|
||||
message=f"Provider '{model.provider}' has no base_url configured.",
|
||||
)
|
||||
return YuanqiASRWhisper(
|
||||
base_url=base_url,
|
||||
api_key=api_key or "",
|
||||
model=model.model,
|
||||
request_timeout_seconds=model.timeout_seconds or 300,
|
||||
)
|
||||
|
||||
# MiMo 等走 /chat/completions + input_audio
|
||||
from app.ai_provider.audio.asr import HunjingASROpenAI
|
||||
|
||||
if model.endpoints:
|
||||
return HunjingASROpenAI.from_model_config(
|
||||
model, provider=self._provider_or_none(model)
|
||||
)
|
||||
|
||||
provider = self._manager.get_provider_config(model.provider)
|
||||
api_key, base_url = self._resolve_credentials(provider)
|
||||
if not base_url:
|
||||
raise AudioServiceError(
|
||||
status_code=500,
|
||||
code="audio_asr_no_base_url",
|
||||
message=f"Provider '{model.provider}' has no base_url configured.",
|
||||
)
|
||||
return HunjingASROpenAI(
|
||||
base_url=base_url,
|
||||
api_key=api_key or "",
|
||||
model=model.model,
|
||||
request_timeout_seconds=model.timeout_seconds or 300,
|
||||
)
|
||||
|
||||
# ── TTS ─────────────────────────────────────────────────────────────
|
||||
|
||||
def _create_tts(self, model):
|
||||
from app.ai_provider.audio.tts import HunjingTTSOpenAI
|
||||
|
||||
# 有 endpoints 时交给 from_model_config(按端点的 status/model/超时)
|
||||
if model.endpoints:
|
||||
return HunjingTTSOpenAI.from_model_config(
|
||||
model, provider=self._provider_or_none(model)
|
||||
)
|
||||
|
||||
# 无 endpoint 时回退到 provider 级别凭证
|
||||
provider = self._manager.get_provider_config(model.provider)
|
||||
api_key, base_url = self._resolve_credentials(provider)
|
||||
if not base_url:
|
||||
raise AudioServiceError(
|
||||
status_code=500,
|
||||
code="audio_tts_no_base_url",
|
||||
message=f"Provider '{model.provider}' has no base_url configured.",
|
||||
)
|
||||
return HunjingTTSOpenAI(
|
||||
base_url=base_url,
|
||||
api_key=api_key or "",
|
||||
model=model.model,
|
||||
request_timeout_seconds=model.timeout_seconds or 120,
|
||||
)
|
||||
|
||||
def _resolve_credentials(self, provider):
|
||||
"""从 settings 解析 provider 级别的 API key 和 base URL。"""
|
||||
from app.core.config import settings
|
||||
|
||||
api_key = (
|
||||
settings.__getattribute__(provider.api_key_env.lower())
|
||||
if hasattr(settings, provider.api_key_env.lower())
|
||||
else ""
|
||||
)
|
||||
base_url = (
|
||||
settings.__getattribute__(provider.base_url_env.lower())
|
||||
if hasattr(settings, provider.base_url_env.lower())
|
||||
else None
|
||||
)
|
||||
return api_key, base_url
|
||||
|
||||
|
||||
class TTSRequest(BaseModel):
|
||||
"""TTS 合成请求。"""
|
||||
|
||||
text: str = Field(..., min_length=1, max_length=4096, description="要合成语音的文本。")
|
||||
voice: str = Field("default", description="语音角色标识(取决于模型,如 default 或声纹 ID)。")
|
||||
format: str = Field("wav", description="音频格式:wav / mp3 / opus / flac。")
|
||||
|
||||
|
||||
@router.post("/audio/tts")
|
||||
async def synthesize_speech(body: TTSRequest) -> Response:
|
||||
"""合成文本为语音,返回音频二进制流。
|
||||
|
||||
使用神识注册表中第一个活跃的 TTS 模型(capability: tts)。
|
||||
未配置 TTS 模型时返回 503。
|
||||
"""
|
||||
if not body.text.strip():
|
||||
raise HTTPException(status_code=400, detail="text 不能为空。")
|
||||
|
||||
mime = _MIME_MAP.get(body.format, "audio/wav")
|
||||
|
||||
try:
|
||||
manager = ProviderManagerFactory.create()
|
||||
service = AudioService(_AudioModelFactory(manager))
|
||||
async with service:
|
||||
result = await service.synthesize(
|
||||
text=body.text,
|
||||
voice=body.voice,
|
||||
response_format=body.format,
|
||||
)
|
||||
except AudioServiceError as exc:
|
||||
logger.error(
|
||||
"audio.tts.api_error",
|
||||
voice=body.voice,
|
||||
format=body.format,
|
||||
error_code=exc.code,
|
||||
error_message=exc.message,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=exc.status_code or 502,
|
||||
detail=exc.message,
|
||||
) from exc
|
||||
|
||||
logger.info(
|
||||
"audio.tts.success",
|
||||
voice=body.voice,
|
||||
format=body.format,
|
||||
audio_bytes=len(result.audio_data),
|
||||
)
|
||||
|
||||
return Response(
|
||||
content=result.audio_data,
|
||||
media_type=mime,
|
||||
headers={
|
||||
"Content-Disposition": f'inline; filename="speech.{body.format}"',
|
||||
"X-Audio-Format": body.format,
|
||||
"Cache-Control": "public, max-age=3600",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ── ASR(语音转文本)───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TranscriptionResponse(BaseModel):
|
||||
"""OpenAI Whisper 兼容的转写响应。"""
|
||||
|
||||
text: str = Field(description="转写后的文本")
|
||||
|
||||
|
||||
@router.post("/audio/transcriptions", response_model=TranscriptionResponse)
|
||||
async def transcribe_audio(
|
||||
file: UploadFile = File(..., description="音频文件(wav/mp3/m4a/flac/ogg/opus/webm 等)"),
|
||||
language: str | None = Form(default=None, description="语言提示(ISO-639-1:zh/en/ja/ko 等),省略则自动检测"),
|
||||
model: str | None = Form(default=None, description="可选,指定使用的 ASR 模型逻辑名(如 funasr-sensevoice / mimo-v2.5-asr),省略则用注册表中第一个"),
|
||||
) -> TranscriptionResponse:
|
||||
"""转写音频为文本,返回 OpenAI Whisper 兼容的 JSON。
|
||||
|
||||
不指定 ``model`` 时用神识注册表中第一个活跃的 ASR 模型(capability: asr),
|
||||
按其 ``provider`` 自动选择协议适配器:``yuanqi`` 走本地 multipart 端点,
|
||||
其他走 MiMo 风格的 chat completions。未配置 ASR 模型时返回 503。
|
||||
"""
|
||||
audio_bytes = await file.read()
|
||||
if not audio_bytes:
|
||||
raise HTTPException(status_code=400, detail="音频文件为空。")
|
||||
|
||||
fmt = (file.filename or "audio.wav").rsplit(".", 1)[-1].lower() if "." in (file.filename or "") else "wav"
|
||||
|
||||
try:
|
||||
manager = ProviderManagerFactory.create()
|
||||
service = AudioService(_AudioModelFactory(manager))
|
||||
async with service:
|
||||
result = await service.transcribe(
|
||||
audio_data=audio_bytes,
|
||||
format=fmt,
|
||||
language=language,
|
||||
model_name=model,
|
||||
)
|
||||
except AudioServiceError as exc:
|
||||
logger.error(
|
||||
"audio.asr.api_error",
|
||||
filename=file.filename,
|
||||
format=fmt,
|
||||
language=language,
|
||||
error_code=exc.code,
|
||||
error_message=exc.message,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=exc.status_code or 502,
|
||||
detail=exc.message,
|
||||
) from exc
|
||||
|
||||
logger.info(
|
||||
"audio.asr.success",
|
||||
filename=file.filename,
|
||||
format=fmt,
|
||||
language=language,
|
||||
text_length=len(result.text),
|
||||
text=result.text,
|
||||
)
|
||||
|
||||
return TranscriptionResponse(text=result.text)
|
||||
@@ -1,32 +1,23 @@
|
||||
"""Run 与 SSE API。"""
|
||||
"""Run 与 SSE API(Run 的启动契约见 kernel_runs.py)。"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Header, Query, Request
|
||||
from fastapi import APIRouter, Header, Query
|
||||
from sse_starlette.sse import EventSourceResponse
|
||||
|
||||
from app.application.agent.confirmation import ConfirmationRegistry
|
||||
from app.application.agent.run_registry import AgentRunRegistry
|
||||
from app.core.config import settings
|
||||
from app.core.errors import AppError
|
||||
from app.core.time import to_iso, utc_now
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.repositories import runtime as runtime_repo
|
||||
from app.schemas.common import Page
|
||||
from app.schemas.runtime import (
|
||||
AgentControlRequest,
|
||||
AgentControlResponse,
|
||||
ConfirmationResolutionResponse,
|
||||
EventResponse,
|
||||
RunResponse,
|
||||
)
|
||||
from app.schemas.runtime import EventResponse, RunResponse
|
||||
from app.services.event_bus import bus
|
||||
from app.services.runtime import (
|
||||
cancel_run,
|
||||
confirmation_exists_in_run,
|
||||
get_run_response,
|
||||
list_run_event_responses,
|
||||
list_runs_response,
|
||||
@@ -66,107 +57,11 @@ async def list_run_events(
|
||||
return await list_run_event_responses(run_id, limit=limit, offset=offset)
|
||||
|
||||
|
||||
def _get_agent_run_registry(request: Request) -> AgentRunRegistry:
|
||||
registry: AgentRunRegistry | None = getattr(request.app.state, "agent_run_registry", None)
|
||||
if registry is None:
|
||||
raise AppError("service_unavailable", "Agent 运行控制未启用。", status_code=503)
|
||||
return registry
|
||||
|
||||
|
||||
@router.post("/{run_id}/cancel", response_model=RunResponse)
|
||||
async def cancel_run_api(run_id: str, request: Request) -> RunResponse:
|
||||
registry = getattr(request.app.state, "agent_run_registry", None)
|
||||
if registry is not None:
|
||||
await registry.abort(run_id)
|
||||
async def cancel_run_api(run_id: str) -> RunResponse:
|
||||
return await cancel_run(run_id)
|
||||
|
||||
|
||||
@router.post("/{run_id}/steer", response_model=AgentControlResponse)
|
||||
async def steer_run(
|
||||
run_id: str,
|
||||
payload: AgentControlRequest,
|
||||
request: Request,
|
||||
) -> AgentControlResponse:
|
||||
if not _get_agent_run_registry(request).steer(run_id, payload.content):
|
||||
raise AppError("conflict", "Run 当前不在本进程执行,无法 steer。", status_code=409)
|
||||
return AgentControlResponse()
|
||||
|
||||
|
||||
@router.post("/{run_id}/follow-up", response_model=AgentControlResponse)
|
||||
async def follow_up_run(
|
||||
run_id: str,
|
||||
payload: AgentControlRequest,
|
||||
request: Request,
|
||||
) -> AgentControlResponse:
|
||||
if not _get_agent_run_registry(request).follow_up(run_id, payload.content):
|
||||
raise AppError("conflict", "Run 当前不在本进程执行,无法追加 follow-up。", status_code=409)
|
||||
return AgentControlResponse()
|
||||
|
||||
|
||||
def _get_confirmation_registry(request: Request) -> ConfirmationRegistry:
|
||||
"""从 app.state 取 ConfirmationRegistry;未挂载时 503。"""
|
||||
registry: ConfirmationRegistry | None = getattr(
|
||||
request.app.state, "confirmation_registry", None
|
||||
)
|
||||
if registry is None:
|
||||
raise AppError(
|
||||
"service_unavailable",
|
||||
"确认机制未启用(进程内 Future 注册表未挂载)",
|
||||
status_code=503,
|
||||
)
|
||||
return registry
|
||||
|
||||
|
||||
async def _resolve_confirmation(
|
||||
request: Request,
|
||||
run_id: str,
|
||||
confirmation_id: str,
|
||||
approved: bool,
|
||||
) -> ConfirmationResolutionResponse:
|
||||
"""approve/deny 共用逻辑:通过 ConfirmationRegistry 更新确认状态。"""
|
||||
registry = _get_confirmation_registry(request)
|
||||
if await registry.resolve(run_id, approved, confirmation_id=confirmation_id):
|
||||
return ConfirmationResolutionResponse(approved=approved)
|
||||
|
||||
# 记录已处理/不存在——查 events 表区分 410 与 404
|
||||
event_store = getattr(request.app.state, "event_store", None)
|
||||
if event_store is not None and await confirmation_exists_in_run(
|
||||
event_store, run_id, confirmation_id
|
||||
):
|
||||
raise AppError(
|
||||
"confirmation_expired",
|
||||
"确认已失效(服务可能已重启或已处理),请重新发起对话",
|
||||
status_code=410,
|
||||
)
|
||||
raise AppError("confirmation_not_found", "确认项不存在", status_code=404)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{run_id}/confirmations/{confirmation_id}/approve",
|
||||
response_model=ConfirmationResolutionResponse,
|
||||
)
|
||||
async def approve_confirmation(
|
||||
run_id: str,
|
||||
confirmation_id: str,
|
||||
request: Request,
|
||||
) -> ConfirmationResolutionResponse:
|
||||
"""批准高风险工具执行:触发 Agent 循环恢复(confirmed=True 重试)。"""
|
||||
return await _resolve_confirmation(request, run_id, confirmation_id, approved=True)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{run_id}/confirmations/{confirmation_id}/deny",
|
||||
response_model=ConfirmationResolutionResponse,
|
||||
)
|
||||
async def deny_confirmation(
|
||||
run_id: str,
|
||||
confirmation_id: str,
|
||||
request: Request,
|
||||
) -> ConfirmationResolutionResponse:
|
||||
"""拒绝高风险工具执行:Agent 循环回填拒绝 ToolMessage。"""
|
||||
return await _resolve_confirmation(request, run_id, confirmation_id, approved=False)
|
||||
|
||||
|
||||
@router.get("/{run_id}/stream")
|
||||
async def stream_run(
|
||||
run_id: str,
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
"""安全 API(api-spec §16)。
|
||||
|
||||
audit-events 按人物/枢过滤查询审计时间线;confirmations 的 approve/deny 负责
|
||||
高风险工具确认:通过 ConfirmationRegistry 更新工具确认状态唤醒 Agent 循环。
|
||||
audit-events 按人物/枢过滤查询审计时间线;confirmations 的 approve/deny 落库裁决
|
||||
高风险工具确认(pending → approved/denied),客户端随后以 confirmed=True 重新调用。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from app.api.v1.deps import get_confirmation_registry, get_optional_tool_runtime
|
||||
from app.application.agent.confirmation import ConfirmationRegistry
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
|
||||
from sqlalchemy import select, update
|
||||
|
||||
from app.api.v1.deps import get_optional_tool_runtime
|
||||
from app.application.tool_runtime.runtime import ToolRuntime
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.safety import PendingConfirmationRecord
|
||||
from app.schemas.admin import AuditEntry
|
||||
from app.schemas.common import Page
|
||||
from app.schemas.safety import (
|
||||
@@ -102,47 +106,22 @@ def _cancelled(confirmation_id: str) -> dict[str, str]:
|
||||
|
||||
|
||||
@router.post("/confirmations/{confirmation_id}/approve")
|
||||
async def approve(
|
||||
confirmation_id: str,
|
||||
registry: ConfirmationRegistry = Depends(get_confirmation_registry),
|
||||
) -> dict[str, bool | str]:
|
||||
"""批准工具执行。通过 ConfirmationRegistry 唤醒等待的 Agent 循环。"""
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.safety import PendingConfirmationRecord
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await session.execute(
|
||||
select(PendingConfirmationRecord).where(PendingConfirmationRecord.id == confirmation_id)
|
||||
)
|
||||
record = result.scalar_one_or_none()
|
||||
if record is None or record.run_id is None:
|
||||
raise _not_found(confirmation_id)
|
||||
|
||||
# 更新 resolution 并唤醒进程内 Event
|
||||
ok = await registry.resolve(
|
||||
record.run_id,
|
||||
approved=True,
|
||||
confirmation_id=confirmation_id,
|
||||
)
|
||||
if not ok:
|
||||
raise _not_found(confirmation_id)
|
||||
|
||||
async def approve(confirmation_id: str) -> dict[str, bool | str]:
|
||||
"""批准工具执行:pending 记录落库为 approved,客户端以 confirmed=True 重新调用。"""
|
||||
record = await _resolve_confirmation_record(confirmation_id, approved=True)
|
||||
return {"status": "resolved", "approved": True, "run_id": record.run_id}
|
||||
|
||||
|
||||
@router.post("/confirmations/{confirmation_id}/deny")
|
||||
async def deny(
|
||||
confirmation_id: str,
|
||||
registry: ConfirmationRegistry = Depends(get_confirmation_registry),
|
||||
) -> dict[str, str]:
|
||||
"""拒绝工具执行。通过 ConfirmationRegistry 通知 Agent 循环。"""
|
||||
from sqlalchemy import select
|
||||
async def deny(confirmation_id: str) -> dict[str, str]:
|
||||
"""拒绝工具执行:pending 记录落库为 denied。"""
|
||||
await _resolve_confirmation_record(confirmation_id, approved=False)
|
||||
return {"status": "cancelled", "confirmation_id": confirmation_id}
|
||||
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.safety import PendingConfirmationRecord
|
||||
|
||||
async def _resolve_confirmation_record(
|
||||
confirmation_id: str, *, approved: bool
|
||||
) -> PendingConfirmationRecord:
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await session.execute(
|
||||
select(PendingConfirmationRecord).where(PendingConfirmationRecord.id == confirmation_id)
|
||||
@@ -151,12 +130,20 @@ async def deny(
|
||||
if record is None or record.run_id is None:
|
||||
raise _not_found(confirmation_id)
|
||||
|
||||
ok = await registry.resolve(
|
||||
record.run_id,
|
||||
approved=False,
|
||||
confirmation_id=confirmation_id,
|
||||
)
|
||||
if not ok:
|
||||
raise _not_found(confirmation_id)
|
||||
|
||||
return {"status": "cancelled", "confirmation_id": confirmation_id}
|
||||
async with AsyncSessionLocal() as session:
|
||||
stmt = (
|
||||
update(PendingConfirmationRecord)
|
||||
.where(
|
||||
PendingConfirmationRecord.id == confirmation_id,
|
||||
PendingConfirmationRecord.status == "pending",
|
||||
)
|
||||
.values(
|
||||
status="approved" if approved else "denied",
|
||||
resolved_at=datetime.now(UTC),
|
||||
)
|
||||
)
|
||||
updated = await session.execute(stmt)
|
||||
await session.commit()
|
||||
if updated.rowcount == 0:
|
||||
raise _not_found(confirmation_id)
|
||||
return record
|
||||
@@ -1,60 +1,25 @@
|
||||
"""设置 API(模型网关状态 + 生成偏好)。
|
||||
"""设置 API(生成偏好)。
|
||||
|
||||
对应前端 settingsApi。模型网关为只读总览,生成偏好可读可写;
|
||||
真实密钥不进入响应。业务逻辑在 services/preferences(与系统配置 core/config/settings 区分)。
|
||||
生成偏好可读可写;模型网关状态由神识承接(用户面见神识阁)。
|
||||
业务逻辑在 services/preferences(与系统配置 core/config/settings 区分)。
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.api.v1.deps import get_current_user
|
||||
from app.core.errors import AppError
|
||||
from app.schemas.auth import CurrentUser
|
||||
from app.schemas.preferences import (
|
||||
GenerationPreferencesResponse,
|
||||
ModelEndpointStatus,
|
||||
ModelGatewayStatus,
|
||||
UpdateEndpointOverrideRequest,
|
||||
UpdateGenerationPreferencesRequest,
|
||||
)
|
||||
from app.services.ai_provider_admin import update_endpoint_override
|
||||
from app.services.preferences import (
|
||||
get_generation_preferences,
|
||||
get_provider_gateway,
|
||||
update_generation_preferences,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/settings", tags=["settings"])
|
||||
|
||||
|
||||
@router.get("/models", response_model=ModelGatewayStatus)
|
||||
async def read_provider_gateway() -> ModelGatewayStatus:
|
||||
return get_provider_gateway()
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/models/{model}/endpoints/{endpoint}",
|
||||
response_model=ModelEndpointStatus,
|
||||
)
|
||||
async def update_model_endpoint(
|
||||
model: str,
|
||||
endpoint: str,
|
||||
payload: UpdateEndpointOverrideRequest,
|
||||
) -> ModelEndpointStatus:
|
||||
"""更新 endpoint 覆盖(base_url / api_key / status / weight / priority,DB 优先 env)。
|
||||
|
||||
写后热重载 registry(reset_model_router)。响应脱敏,不回 base_url / api_key 原值。
|
||||
"""
|
||||
await update_endpoint_override(model, endpoint, payload)
|
||||
gateway = get_provider_gateway()
|
||||
for m in gateway.models:
|
||||
if m.name != model:
|
||||
continue
|
||||
for ep in m.endpoints or []:
|
||||
if ep.name == endpoint:
|
||||
return ep
|
||||
raise AppError("not_found", "endpoint 不存在。")
|
||||
|
||||
|
||||
@router.get("/preferences", response_model=GenerationPreferencesResponse)
|
||||
async def read_generation_preferences(
|
||||
current_user: CurrentUser = Depends(get_current_user),
|
||||
|
||||
Reference in new issue
Block a user