"""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)