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,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,
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user