Files
ai-video-fullstack/backend/routes/model_registry.py
Xin Wang 90e3e8a0c0 Refactor backend to support interface-definition driven model resources
- Introduce a new model structure for managing interface definitions and model resources, enhancing the backend's capability to handle various service integrations.
- Update the Makefile to reflect changes in database seeding and resource management commands.
- Remove the deprecated credentials management routes and replace them with a unified model registry API.
- Modify existing routes and schemas to align with the new model structure, ensuring seamless integration with the frontend.
- Enhance database seeding scripts to populate new model resources and their configurations.
- Update README documentation to reflect the new architecture and usage instructions for model resources and interface definitions.
2026-06-14 19:36:12 +08:00

250 lines
8.1 KiB
Python

"""Interface-definition driven model resource registry APIs."""
import uuid
from db.models import (
AssistantModelBinding,
InterfaceDefinition,
KnowledgeBase,
ModelResource,
)
from db.session import get_session
from fastapi import APIRouter, Depends, HTTPException, Query
from schemas import (
InterfaceDefinitionOut,
ModelResourceOut,
ModelResourceTestResult,
ModelResourceUpsert,
)
from services.interface_catalog import validate_fields
from services.masking import mask_secrets, merge_secrets
from services.model_resource_tester import test_model_resource
from sqlalchemy import delete, select, update
from sqlalchemy.ext.asyncio import AsyncSession
router = APIRouter(prefix="/api", tags=["model-registry"])
def _definition_dict(row: InterfaceDefinition) -> dict:
return {
"interface_type": row.interface_type,
"name": row.name,
"capability": row.capability,
"fields": (row.field_schema or {}).get("fields", []),
}
def _definition_out(row: InterfaceDefinition) -> InterfaceDefinitionOut:
return InterfaceDefinitionOut(
interface_type=row.interface_type,
name=row.name,
capability=row.capability, # type: ignore[arg-type]
field_schema=row.field_schema or {},
enabled=row.enabled,
version=row.version,
)
def _resource_out(row: ModelResource) -> ModelResourceOut:
return ModelResourceOut(
id=row.id,
name=row.name,
capability=row.capability, # type: ignore[arg-type]
interface_type=row.interface_type,
values=row.values or {},
secrets=mask_secrets(row.secrets or {}),
enabled=row.enabled,
is_default=row.is_default,
updated_at=row.updated_at.isoformat() if row.updated_at else None,
)
async def _definition(
session: AsyncSession, interface_type: str
) -> InterfaceDefinition:
row = await session.get(InterfaceDefinition, interface_type)
if not row or not row.enabled:
raise HTTPException(400, f"接口类型不可用: {interface_type}")
return row
async def _validate(
session: AsyncSession,
body: ModelResourceUpsert,
stored_secrets: dict | None = None,
) -> tuple[InterfaceDefinition, dict]:
definition = await _definition(session, body.interface_type)
secrets = merge_secrets(body.secrets, stored_secrets or {})
try:
validate_fields(_definition_dict(definition), body.values, secrets)
except ValueError as exc:
raise HTTPException(422, str(exc)) from exc
return definition, secrets
async def _clear_incompatible_references(
session: AsyncSession, resource: ModelResource, capability: str
) -> None:
if capability == resource.capability:
return
await session.execute(
delete(AssistantModelBinding).where(
AssistantModelBinding.model_resource_id == resource.id
)
)
await session.execute(
update(KnowledgeBase)
.where(KnowledgeBase.embedding_model_resource_id == resource.id)
.values(embedding_model_resource_id=None)
)
@router.get("/interface-definitions", response_model=list[InterfaceDefinitionOut])
async def list_interface_definitions(
capability: str | None = Query(default=None),
session: AsyncSession = Depends(get_session),
):
stmt = select(InterfaceDefinition).where(InterfaceDefinition.enabled.is_(True))
if capability:
stmt = stmt.where(InterfaceDefinition.capability == capability)
rows = (await session.execute(stmt.order_by(InterfaceDefinition.capability))).scalars().all()
return [_definition_out(row) for row in rows]
@router.get("/model-resources", response_model=list[ModelResourceOut])
async def list_model_resources(session: AsyncSession = Depends(get_session)):
rows = (
await session.execute(select(ModelResource).order_by(ModelResource.capability))
).scalars().all()
return [_resource_out(row) for row in rows]
@router.post("/model-resources", response_model=ModelResourceOut)
async def create_model_resource(
body: ModelResourceUpsert, session: AsyncSession = Depends(get_session)
):
definition, secrets = await _validate(session, body)
row = ModelResource(
id=f"model_{uuid.uuid4().hex[:12]}",
name=body.name,
capability=definition.capability,
interface_type=definition.interface_type,
values=body.values,
secrets=secrets,
enabled=body.enabled,
is_default=body.is_default,
)
session.add(row)
if row.is_default:
await session.execute(
update(ModelResource)
.where(ModelResource.capability == row.capability, ModelResource.id != row.id)
.values(is_default=False)
)
await session.commit()
return _resource_out(row)
@router.post("/model-resources/test", response_model=ModelResourceTestResult)
async def test_new_model_resource(
body: ModelResourceUpsert, session: AsyncSession = Depends(get_session)
):
definition, secrets = await _validate(session, body)
return await test_model_resource(
definition.interface_type,
definition.capability,
body.values,
secrets,
)
@router.post(
"/model-resources/{resource_id}/test", response_model=ModelResourceTestResult
)
async def test_saved_model_resource(
resource_id: str,
body: ModelResourceUpsert,
session: AsyncSession = Depends(get_session),
):
row = await session.get(ModelResource, resource_id)
if not row:
raise HTTPException(404, "模型资源不存在")
definition, secrets = await _validate(session, body, row.secrets or {})
return await test_model_resource(
definition.interface_type,
definition.capability,
body.values,
secrets,
)
@router.post("/model-resources/{resource_id}/duplicate", response_model=ModelResourceOut)
async def duplicate_model_resource(
resource_id: str, session: AsyncSession = Depends(get_session)
):
source = await session.get(ModelResource, resource_id)
if not source:
raise HTTPException(404, "模型资源不存在")
row = ModelResource(
id=f"model_{uuid.uuid4().hex[:12]}",
name=f"{source.name} 副本",
capability=source.capability,
interface_type=source.interface_type,
values=dict(source.values or {}),
secrets=dict(source.secrets or {}),
enabled=source.enabled,
is_default=False,
)
session.add(row)
await session.commit()
return _resource_out(row)
@router.put("/model-resources/{resource_id}", response_model=ModelResourceOut)
async def update_model_resource(
resource_id: str,
body: ModelResourceUpsert,
session: AsyncSession = Depends(get_session),
):
row = await session.get(ModelResource, resource_id)
if not row:
raise HTTPException(404, "模型资源不存在")
definition, secrets = await _validate(session, body, row.secrets or {})
await _clear_incompatible_references(session, row, definition.capability)
row.name = body.name
row.capability = definition.capability
row.interface_type = definition.interface_type
row.values = body.values
row.secrets = secrets
row.enabled = body.enabled
row.is_default = body.is_default
if row.is_default:
await session.execute(
update(ModelResource)
.where(ModelResource.capability == row.capability, ModelResource.id != row.id)
.values(is_default=False)
)
await session.commit()
return _resource_out(row)
@router.delete("/model-resources/{resource_id}")
async def delete_model_resource(
resource_id: str, session: AsyncSession = Depends(get_session)
):
row = await session.get(ModelResource, resource_id)
if not row:
raise HTTPException(404, "模型资源不存在")
in_use = (
await session.execute(
select(AssistantModelBinding.assistant_id)
.where(AssistantModelBinding.model_resource_id == resource_id)
.limit(1)
)
).scalar_one_or_none()
if in_use:
raise HTTPException(409, "该模型资源仍被助手引用")
await session.delete(row)
await session.commit()
return {"ok": True}