bifroest/app/ai/models.py
2026-10-27 08:34:17 +01:00

72 lines
3.3 KiB
Python

"""Endpunkte, API-Keys und Modelle (→ Konzept 8.1).
Global gepflegt vom Plattform-Admin. Ein Key gilt entweder für alle Teams
(team_id leer, z. B. OpenAI in der Entwicklung) oder für genau ein Team
(Gateway). Der Team-Key hat Vorrang.
"""
from datetime import datetime
from decimal import Decimal
from sqlalchemy import Boolean, ForeignKey, Index, Integer, Numeric, String, UniqueConstraint, text
from sqlalchemy.orm import Mapped, mapped_column
from app.core.database import Base, now
# Modellklassen (→ Konzept 8.1); zunächst zeigen alle auf dasselbe Modell
MODEL_CLASSES = ("standard", "reasoning", "fast")
class Provider(Base):
__tablename__ = "providers"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100), unique=True)
# OpenAI-kompatibel mit Responses API: OpenAI direkt oder das AI Gateway
base_url: Mapped[str] = mapped_column(String(500))
active: Mapped[bool] = mapped_column(Boolean, default=True)
created: Mapped[datetime] = mapped_column(default=now)
class ProviderKey(Base):
__tablename__ = "provider_keys"
__table_args__ = (
UniqueConstraint("provider_id", "team_id"),
# Nur ein Key für alle Teams je Endpunkt (NULL zählt sonst nicht als doppelt)
Index("uq_provider_keys_default", "provider_id", unique=True, postgresql_where=text("team_id IS NULL")),
)
id: Mapped[int] = mapped_column(primary_key=True)
provider_id: Mapped[int] = mapped_column(ForeignKey("providers.id", ondelete="CASCADE"))
team_id: Mapped[int | None] = mapped_column(ForeignKey("teams.id", ondelete="CASCADE"))
# Verschlüsselt (app.core.crypto); angezeigt werden nur die letzten Zeichen
secret: Mapped[str] = mapped_column(String(2000))
hint: Mapped[str] = mapped_column(String(10))
updated_by: Mapped[int] = mapped_column(ForeignKey("users.id"))
updated: Mapped[datetime] = mapped_column(default=now)
class Model(Base):
__tablename__ = "models"
__table_args__ = (
UniqueConstraint("provider_id", "name"),
Index("uq_models_default", "is_default", unique=True, postgresql_where=text("is_default")),
)
id: Mapped[int] = mapped_column(primary_key=True)
provider_id: Mapped[int] = mapped_column(ForeignKey("providers.id", ondelete="CASCADE"))
# Name, wie ihn die API erwartet, z. B. gpt-6-luna
name: Mapped[str] = mapped_column(String(200))
label: Mapped[str] = mapped_column(String(200))
model_class: Mapped[str] = mapped_column(String(20), default="standard")
context_window: Mapped[int] = mapped_column(Integer, default=128000)
supports_tools: Mapped[bool] = mapped_column(Boolean, default=True)
supports_reasoning: Mapped[bool] = mapped_column(Boolean, default=False)
# Preise je 1 Mio. Tokens in cost_currency; leer = Kosten werden nicht berechnet
input_price: Mapped[Decimal | None] = mapped_column(Numeric(12, 4))
cached_price: Mapped[Decimal | None] = mapped_column(Numeric(12, 4))
output_price: Mapped[Decimal | None] = mapped_column(Numeric(12, 4))
# Nur aktive Modelle sind für Workspaces wählbar
active: Mapped[bool] = mapped_column(Boolean, default=True)
is_default: Mapped[bool] = mapped_column(Boolean, default=False)
created: Mapped[datetime] = mapped_column(default=now)