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:
Jaroslav Beneš
2026-07-21 10:34:48 +02:00
parent 0665027bc6
commit 5ef2af6a9f
32 changed files with 2041 additions and 0 deletions
+82
View File
@@ -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}>"