Files
loom/backend/app/models/llm.py
T
2026-04-24 17:46:03 +08:00

121 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""LLM Provider / Model / RoutingRule / Usage。
支持多 Provider(openai / azure / 国产 LLM 等)统一管理;路由规则按优先级命中
provider + model;每次调用在 llm_usage 落 token / 成本。
"""
from __future__ import annotations
from datetime import datetime
from sqlalchemy import (
JSON,
Boolean,
DateTime,
ForeignKey,
Integer,
Numeric,
String,
func,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base, TimestampMixin
class LLMProvider(Base, TimestampMixin):
__tablename__ = "llm_provider"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
code: Mapped[str] = mapped_column(String(64), nullable=False, unique=True)
name: Mapped[str] = mapped_column(String(64), nullable=False)
# openai / azure / anthropic / custom
vendor: Mapped[str] = mapped_column(String(32), nullable=False, default="openai")
api_base: Mapped[str] = mapped_column(String(255), nullable=False)
# 加密存储;由 service 层做加解密
api_key_cipher: Mapped[str | None] = mapped_column(String(512), nullable=True)
description: Mapped[str | None] = mapped_column(String(255), nullable=True)
status: Mapped[str] = mapped_column(String(16), default="ACTIVE", nullable=False)
class LLMModel(Base, TimestampMixin):
__tablename__ = "llm_model"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
provider_id: Mapped[int] = mapped_column(
ForeignKey("llm_provider.id", ondelete="CASCADE"), nullable=False, index=True
)
# 对外展示名,如 "gpt-4o-mini"
model: Mapped[str] = mapped_column(String(128), nullable=False)
name: Mapped[str] = mapped_column(String(128), nullable=False)
description: Mapped[str | None] = mapped_column(String(255), nullable=True)
# 上下文窗口(tokens)
max_context: Mapped[int] = mapped_column(Integer, default=8192, nullable=False)
# 单价(每千 token,USD),用于成本粗估
price_input_per_1k: Mapped[float] = mapped_column(
Numeric(10, 6), default=0, nullable=False
)
price_output_per_1k: Mapped[float] = mapped_column(
Numeric(10, 6), default=0, nullable=False
)
status: Mapped[str] = mapped_column(String(16), default="ACTIVE", nullable=False)
provider: Mapped[LLMProvider] = relationship("LLMProvider", lazy="selectin")
class LLMRoutingRule(Base, TimestampMixin):
__tablename__ = "llm_routing_rule"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
# 优先级:数字越小越先匹配
priority: Mapped[int] = mapped_column(Integer, default=100, nullable=False)
# 匹配条件(JSON dict):如 {"category": "social", "executor_type": "LOBSTER"};为空表示默认
conditions: Mapped[dict | None] = mapped_column(JSON, nullable=True)
model_id: Mapped[int] = mapped_column(
ForeignKey("llm_model.id", ondelete="RESTRICT"), nullable=False
)
enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
description: Mapped[str | None] = mapped_column(String(255), nullable=True)
model_obj: Mapped[LLMModel] = relationship("LLMModel", lazy="selectin")
class LLMUsage(Base):
"""每次 LLM 调用的用量记账。"""
__tablename__ = "llm_usage"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
provider_id: Mapped[int] = mapped_column(
ForeignKey("llm_provider.id", ondelete="SET NULL"), nullable=True, index=True
)
model_id: Mapped[int] = mapped_column(
ForeignKey("llm_model.id", ondelete="SET NULL"), nullable=True, index=True
)
provider_code: Mapped[str | None] = mapped_column(String(64), nullable=True)
model: Mapped[str | None] = mapped_column(String(128), nullable=True)
operator_id: Mapped[int | None] = mapped_column(
ForeignKey("user.id", ondelete="SET NULL"), nullable=True, index=True
)
operator_name: Mapped[str | None] = mapped_column(String(64), nullable=True)
# 任务 / Skill / playground
source: Mapped[str] = mapped_column(String(16), default="playground", nullable=False)
task_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
skill_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
prompt_tokens: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
completion_tokens: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
total_tokens: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
cost_usd: Mapped[float] = mapped_column(Numeric(12, 6), default=0, nullable=False)
# ok / failed
result: Mapped[str] = mapped_column(String(16), default="ok", nullable=False)
message: Mapped[str | None] = mapped_column(String(255), nullable=True)
cost_ms: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False, index=True
)