Scaffold project, data model and artwork
Establish the LLeMbas foundation: FastAPI/Jinja/SQLite layout, the ORM schema, and the original SVG identity. Notable decisions, all recorded in comments at the point they matter: - No Alembic. SQLite only, schema created at startup, so models carry a few columns nothing reads yet (Message.parent_id for branching, content_parts_json for multimodal turns). Adding them later to a live database without migrations is the painful path. - Sessions are server-side rows keyed by a SHA-256 of the cookie value, not JWTs, so logout and bans revoke access immediately. - Upstream API keys are Fernet-encrypted with a key derived from LEMBAS_SECRET_KEY. decrypt() fails soft to "" so rotating the secret degrades to re-entering keys rather than crashing the admin UI. - Artwork is generated by scripts/build_artwork.py rather than hand-drawn per file: the mallorn leaf appears in the icon, favicon, lockup and banner, and one source is the only way those stay in sync. The wordmark is Source Serif 4 (OFL) converted to outlines, because a README banner cannot load a webfont and <text> would render in whatever serif the viewer happens to have. - Icons live in a template partial, not assets/, because same-document <use href="#id"> is universally supported and the cross-document form is not. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
"""OpenAI-compatible endpoint connections and their discovered models."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from lembas.db.base import Base, Timestamps, UUIDPrimaryKey
|
||||
from lembas.db.types import JSONDict
|
||||
|
||||
|
||||
class Connection(UUIDPrimaryKey, Timestamps, Base):
|
||||
"""A configured upstream endpoint speaking the OpenAI HTTP API.
|
||||
|
||||
Works for api.openai.com as well as LM Studio, vLLM, llama.cpp, Ollama's
|
||||
compatibility layer, OpenRouter, and anything else exposing /v1.
|
||||
"""
|
||||
|
||||
__tablename__ = "connections"
|
||||
|
||||
name: Mapped[str] = mapped_column(String(120), nullable=False)
|
||||
base_url: Mapped[str] = mapped_column(String(500), nullable=False)
|
||||
|
||||
# Fernet ciphertext, never the raw key. See lembas.services.crypto.
|
||||
# Empty string is legitimate: local endpoints often need no auth at all.
|
||||
api_key_encrypted: Mapped[str] = mapped_column(Text, default="")
|
||||
|
||||
enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
position: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
||||
|
||||
# Extra headers merged into every request (e.g. OpenRouter's HTTP-Referer).
|
||||
extra_headers_json: Mapped[dict[str, Any]] = mapped_column(JSONDict, default=dict)
|
||||
|
||||
# Result of the most recent "Test & refresh", surfaced in the admin list.
|
||||
last_checked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
last_error: Mapped[str] = mapped_column(Text, default="")
|
||||
|
||||
models: Mapped[list[Model]] = relationship(
|
||||
back_populates="connection",
|
||||
cascade="all, delete-orphan",
|
||||
order_by="Model.model_id",
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Connection {self.name} {self.base_url}>"
|
||||
|
||||
|
||||
class Model(UUIDPrimaryKey, Timestamps, Base):
|
||||
"""A model advertised by a connection, cached locally.
|
||||
|
||||
Cached rather than fetched live so the chat UI stays responsive and keeps
|
||||
working when an endpoint is briefly unreachable. Refreshed on demand from
|
||||
the admin screen.
|
||||
"""
|
||||
|
||||
__tablename__ = "models"
|
||||
__table_args__ = (UniqueConstraint("connection_id", "model_id"),)
|
||||
|
||||
connection_id: Mapped[str] = mapped_column(
|
||||
String(32), ForeignKey("connections.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
model_id: Mapped[str] = mapped_column(String(300), nullable=False)
|
||||
display_name: Mapped[str] = mapped_column(String(300), default="")
|
||||
enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
|
||||
# Endpoints do not reliably advertise capabilities, so these are admin
|
||||
# overrides consumed by later passes (vision uploads, tool calling).
|
||||
capabilities_json: Mapped[dict[str, Any]] = mapped_column(JSONDict, default=dict)
|
||||
# Default sampling params applied to new chats using this model.
|
||||
params_json: Mapped[dict[str, Any]] = mapped_column(JSONDict, default=dict)
|
||||
|
||||
connection: Mapped[Connection] = relationship(back_populates="models")
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return self.display_name or self.model_id
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Model {self.model_id}>"
|
||||
Reference in New Issue
Block a user