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:
chenxin committed 2026-08-27 10:02:44 +08:00
1 parent 0d383725b5
commit 462d570751
129 files changed
+81 -15867

No files matched your search

-108
View File
@@ -1,108 +0,0 @@
"""AI provider 运行时治理:endpoint 覆盖(DB 优先于 .env/yaml)+ 热重载。
override 写 DB → 读 DB 全量刷 spec 同步缓存 → ProviderManager 下次构建读新缓存。
api_key 明文存 DB(本地单用户边界),响应层脱敏不回值(见 schemas)。
"""
from __future__ import annotations
from typing import Any
from app.ai_provider.config.loader import load_models_config
from app.core.errors import AppError
from app.db.session import AsyncSessionLocal
from app.models.governance import ModelEndpointOverride
from app.repositories import model_endpoints as overrides_repo
from app.schemas.preferences import UpdateEndpointOverrideRequest
from app.services.audit import record_audit_event
# —— endpoint 运行时覆盖(DB 优先于 .env/yaml)——
# reload_provider_manager 时(异步 service)读 DB 全量刷此同步缓存,
# load_models_config 仍可独立使用。None 字段表示不覆盖、回退 yaml/env。
_endpoint_overrides_cache: dict[tuple[str, str], dict[str, Any]] = {}
def set_endpoint_overrides_cache(overrides: list[dict[str, Any]]) -> None:
"""用 DB 全量覆盖列表刷新模块缓存(同步)。"""
global _endpoint_overrides_cache
_endpoint_overrides_cache = {
(o["logical_model_name"], o["endpoint_name"]): o for o in overrides
}
def get_endpoint_overrides_cache() -> dict[tuple[str, str], dict[str, Any]]:
"""读取当前覆盖缓存快照。"""
return dict(_endpoint_overrides_cache)
async def list_endpoint_overrides() -> list[dict[str, Any]]:
"""读 DB 全量覆盖,序列化为 dict 列表(供刷缓存 + 响应)。"""
async with AsyncSessionLocal() as session:
rows = await overrides_repo.list_overrides(session)
return [_override_to_dict(r) for r in rows]
async def reload_provider_manager() -> None:
"""读 DB 全量 → 刷 endpoint 覆盖同步缓存。"""
overrides = await list_endpoint_overrides()
set_endpoint_overrides_cache(overrides)
async def update_endpoint_override(
logical_model_name: str,
endpoint_name: str,
payload: UpdateEndpointOverrideRequest,
) -> dict[str, Any]:
"""upsert 覆盖(payload 字段全量写入,None 清除字段)+ reload + 审计。
校验 (model, endpoint) 存在于 yaml(避免覆盖幽灵 endpoint)。
"""
if not _endpoint_exists(logical_model_name, endpoint_name):
raise AppError("not_found", "endpoint 不存在于模型配置中。")
async with AsyncSessionLocal() as session:
override = ModelEndpointOverride(
logical_model_name=logical_model_name,
endpoint_name=endpoint_name,
base_url=payload.base_url,
api_key=payload.api_key,
status=payload.status,
weight=payload.weight,
priority=payload.priority,
)
saved = await overrides_repo.upsert_override(session, override)
await record_audit_event(
session,
action="model_endpoint.override_updated",
actor="user",
input_summary=f"{logical_model_name}/{endpoint_name}",
resource_type="model_endpoint_override",
resource_id=f"{logical_model_name}/{endpoint_name}",
)
await session.commit()
await session.refresh(saved)
result = _override_to_dict(saved)
await reload_provider_manager()
return result
def _endpoint_exists(logical_model_name: str, endpoint_name: str) -> bool:
"""校验 (model, endpoint) 在 yaml 配置中存在(避免覆盖幽灵 endpoint)。"""
models = load_models_config()
for model in models:
if model.name != logical_model_name:
continue
return any(ep.name == endpoint_name for ep in model.endpoints)
return False
def _override_to_dict(row: ModelEndpointOverride) -> dict[str, Any]:
return {
"logical_model_name": row.logical_model_name,
"endpoint_name": row.endpoint_name,
"base_url": row.base_url,
"api_key": row.api_key,
"status": row.status,
"weight": row.weight,
"priority": row.priority,
"updated_at": row.updated_at,
}
+2 -91
View File
@@ -1,11 +1,8 @@
"""设置服务:模型网关状态聚合与生成偏好。
"""设置服务:生成偏好。
模型网关状态从 config/models.yaml 派生,provider 密钥按 settings 是否配置脱敏为
configured 布尔(绝不暴露值);生成偏好为本地单用户单行,无则返回默认值。
生成偏好为本地单用户单行,无则返回默认值;模型网关状态由神识承接(用户面见神识阁)。
"""
from app.ai_provider.config.loader import load_models_config
from app.ai_provider.config.models import ModelConfig
from app.core.config import settings
from app.core.time import utc_now
from app.db.session import AsyncSessionLocal
@@ -13,96 +10,10 @@ from app.models.governance import GenerationPreferences
from app.repositories import preferences as preferences_repo
from app.schemas.preferences import (
GenerationPreferencesResponse,
LogicalModel,
ModelEndpointStatus,
ModelGatewayStatus,
ProviderKeyStatus,
UpdateGenerationPreferencesRequest,
)
from app.services.ai_provider_admin import get_endpoint_overrides_cache
from app.services.serializers import generation_preferences_to_response
# provider → 持有其 API Key 的环境变量名;未登记的 provider 回退 <PROVIDER>_API_KEY。
_PROVIDER_KEY_ENV: dict[str, str] = {
"openai": "OPENAI_API_KEY",
"deepseek": "DEEPSEEK_API_KEY",
"anthropic": "ANTHROPIC_API_KEY",
"google": "GOOGLE_API_KEY",
"minimax": "MINIMAX_API_KEY",
"zhipu": "ZHIPU_API_KEY",
"tencentcloud": "TENCENTCLOUD_API_KEY",
"volcengine": "VOLCENGINE_ARK_API_KEY",
"bailian": "BAILIAN_API_KEY",
}
def provider_configured(provider: str | None) -> bool:
"""provider 的 API Key 是否已在 settings 配置(仅返回布尔,不暴露值)。"""
if not provider:
return False
env_name = _PROVIDER_KEY_ENV.get(provider, f"{provider.upper()}_API_KEY")
return bool(getattr(settings, env_name.lower(), None))
def _endpoint_status(
endpoint, model_name: str, provider: str, overrides_cache
) -> ModelEndpointStatus:
ov = overrides_cache.get((model_name, endpoint.name))
return ModelEndpointStatus(
name=endpoint.name,
provider=provider,
status=(ov.get("status") or endpoint.status) if ov else endpoint.status,
weight=endpoint.weight,
priority=endpoint.priority,
is_overridden=ov is not None,
has_url_override=bool(ov and ov.get("base_url")),
has_key_override=bool(ov and ov.get("api_key")),
base_url_env=endpoint.base_url_env,
api_key_env=endpoint.api_key_env,
)
def _logical_model(cfg: ModelConfig, overrides_cache) -> LogicalModel:
provider = cfg.provider or "unknown"
endpoints = [
_endpoint_status(ep, cfg.name, provider, overrides_cache) for ep in cfg.endpoints
] or None
return LogicalModel(
name=cfg.name,
display_name=cfg.display_name or cfg.name,
provider=provider,
capabilities=[k for k, v in cfg.resolved_capabilities().items() if v],
routing_strategy=cfg.routing_strategy,
supports_streaming=bool(cfg.supports_streaming),
supports_thinking=bool(cfg.supports_thinking),
supports_structured_output=bool(cfg.supports_structured_output),
context_window=cfg.context_window,
max_output_tokens=cfg.max_output_tokens,
endpoints=endpoints,
is_default=True if cfg.name == settings.default_model_hint else None,
)
def get_provider_gateway() -> ModelGatewayStatus:
"""从 config/models.yaml 派生模型网关总览(含 DB 覆盖标记,脱敏不回原值)。"""
models_cfg = load_models_config()
overrides_cache = get_endpoint_overrides_cache()
providers_seen: dict[str, None] = {}
logical_models: list[LogicalModel] = []
for cfg in models_cfg:
logical_models.append(_logical_model(cfg, overrides_cache))
provider = cfg.provider or "unknown"
providers_seen.setdefault(provider, None)
providers = [
ProviderKeyStatus(provider=name, configured=provider_configured(name))
for name in providers_seen
]
return ModelGatewayStatus(
models=logical_models,
providers=providers,
default_model_hint=settings.default_model_hint,
)
def _default_preferences() -> GenerationPreferences:
return GenerationPreferences(
-19
View File
@@ -10,7 +10,6 @@ from app.core.security import redact_value
from app.core.time import utc_now
from app.db.session import AsyncSessionLocal
from app.models.runtime import Event, Run
from app.modules.runtime.events import EventStore
from app.repositories import runtime as runtime_repo
from app.repositories.json_utils import dumps
from app.schemas.common import Page
@@ -21,7 +20,6 @@ from app.services.serializers import event_to_response, run_to_response
TERMINAL_RUN_STATUS = {"completed", "failed", "cancelled", "interrupted"}
TERMINAL_EVENT_TYPES = {"run.completed", "run.failed", "run.cancelled", "run.interrupted"}
REQUIRES_CONFIRMATION_EVENT_TYPE = "tool.requires_confirmation"
async def create_run(
@@ -215,20 +213,3 @@ async def list_run_event_responses(
session, run_id, limit=limit, offset=offset
)
return [event_to_response(event) for event in events]
async def confirmation_exists_in_run(
event_store: EventStore, run_id: str, confirmation_id: str
) -> bool:
"""失效感知:run 下是否曾经发过 ``tool.requires_confirmation`` 且 payload 含该 confirmation_id。
实现走 EventStore.list_by_run + Python 侧过滤,避免 PostgreSQL 专用 JSON 操作符,
兼容本地 SQLite。用于 approve/deny 端点在 Future 已失效时区分 410 与 404。
"""
events = await event_store.list_by_run(run_id)
for event in events:
if event.type != REQUIRES_CONFIRMATION_EVENT_TYPE:
continue
if event.payload.get("confirmation_id") == confirmation_id:
return True
return False