Initial commit: LlamaLink Go rewrite
Complete rewrite from Python/FastAPI to Go/Gin: - Go backend: auth (API keys + bcrypt), llama.cpp subprocess manager, hot-swap multi-model, rate limiting, quota system, webhooks - Vue 3 SPA admin panel (src/) with Tailwind CSS - Deployment: Docker multi-stage, docker-compose, nginx, systemd - GORM/SQLite models: ApiKey, Model, UsageLog, Quota, Webhook - REST API: /api/v1/admin/* (keys, models, chat, usage, health) - Embedded frontend via go:embed (build output at web/dist/) Removed legacy Python artifacts (app/, tests/, pyproject.toml, etc.)
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"github.com/llamalink/llamalink/internal/config"
|
||||
)
|
||||
|
||||
func Open(cfg *config.Config) (*gorm.DB, error) {
|
||||
dsn := strings.TrimPrefix(cfg.DatabaseURL, "sqlite://")
|
||||
if dsn == cfg.DatabaseURL {
|
||||
dsn = cfg.DatabaseURL
|
||||
}
|
||||
|
||||
gormConfig := &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
}
|
||||
|
||||
db, err := gorm.Open(sqlite.Open(dsn), gormConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sqlDB.SetMaxOpenConns(cfg.DatabaseMaxOpenConns)
|
||||
sqlDB.SetMaxIdleConns(cfg.DatabaseMaxIdleConns)
|
||||
sqlDB.SetConnMaxLifetime(time.Duration(cfg.DatabaseConnMaxLifetime) * time.Second)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func Migrate(db *gorm.DB) error {
|
||||
slog.Info("running database migrations")
|
||||
return db.AutoMigrate(
|
||||
&ApiKey{},
|
||||
&Model{},
|
||||
&UsageLog{},
|
||||
&Quota{},
|
||||
&Webhook{},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type StringArray []string
|
||||
|
||||
func (a StringArray) Value() (driver.Value, error) {
|
||||
return json.Marshal(a)
|
||||
}
|
||||
|
||||
func (a *StringArray) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
*a = []string{}
|
||||
return nil
|
||||
}
|
||||
b, ok := value.([]byte)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(b, a)
|
||||
}
|
||||
|
||||
type ApiKey struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
|
||||
Name string `gorm:"size:255;not null" json:"name"`
|
||||
KeyHash string `gorm:"size:255;not null" json:"-"`
|
||||
KeyPrefix string `gorm:"size:8;not null;index" json:"key_prefix"`
|
||||
Scopes StringArray `gorm:"type:text;serializer:json" json:"scopes"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
IsAdmin bool `gorm:"default:false" json:"is_admin"`
|
||||
OwnerLabel *string `gorm:"size:255" json:"owner_label,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
UsageLogs []UsageLog `gorm:"foreignKey:ApiKeyID" json:"-"`
|
||||
Quotas []Quota `gorm:"foreignKey:ApiKeyID" json:"-"`
|
||||
Webhooks []Webhook `gorm:"foreignKey:ApiKeyID" json:"-"`
|
||||
}
|
||||
|
||||
func (ApiKey) TableName() string { return "api_keys" }
|
||||
|
||||
func (k *ApiKey) BeforeCreate(tx *gorm.DB) error {
|
||||
if k.ID == uuid.Nil {
|
||||
k.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
|
||||
Name string `gorm:"size:255;uniqueIndex;not null" json:"name"`
|
||||
ModelPath string `gorm:"size:1024;not null" json:"model_path"`
|
||||
Alias string `gorm:"size:255;not null" json:"alias"`
|
||||
CtxSize int `gorm:"default:8192" json:"ctx_size"`
|
||||
NGPULayers int `gorm:"default:-1" json:"n_gpu_layers"`
|
||||
ExtraArgs StringArray `gorm:"type:text;serializer:json" json:"extra_args,omitempty"`
|
||||
IsDefault bool `gorm:"default:false" json:"is_default"`
|
||||
IsEnabled bool `gorm:"default:true" json:"is_enabled"`
|
||||
IsActive bool `gorm:"default:false" json:"is_active"`
|
||||
LoadedAt *time.Time `json:"loaded_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
func (Model) TableName() string { return "models" }
|
||||
|
||||
func (m *Model) BeforeCreate(tx *gorm.DB) error {
|
||||
if m.ID == uuid.Nil {
|
||||
m.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type UsageLog struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
|
||||
ApiKeyID uuid.UUID `gorm:"type:uuid;not null;index" json:"api_key_id"`
|
||||
ModelName string `gorm:"size:255;not null" json:"model_name"`
|
||||
Endpoint string `gorm:"size:100;not null" json:"endpoint"`
|
||||
PromptTokens int `gorm:"default:0" json:"prompt_tokens"`
|
||||
CompletionTokens int `gorm:"default:0" json:"completion_tokens"`
|
||||
TotalTokens int `gorm:"default:0" json:"total_tokens"`
|
||||
LatencyMs int `gorm:"default:0" json:"latency_ms"`
|
||||
Status string `gorm:"size:50;not null" json:"status"`
|
||||
IPAddress *string `gorm:"size:45" json:"ip_address,omitempty"`
|
||||
UserAgent *string `gorm:"size:512" json:"user_agent,omitempty"`
|
||||
ErrorMessage *string `gorm:"type:text" json:"error_message,omitempty"`
|
||||
Streamed bool `gorm:"default:false" json:"streamed"`
|
||||
CreatedAt time.Time `gorm:"index" json:"created_at"`
|
||||
}
|
||||
|
||||
func (UsageLog) TableName() string { return "usage_logs" }
|
||||
|
||||
func (u *UsageLog) BeforeCreate(tx *gorm.DB) error {
|
||||
if u.ID == uuid.Nil {
|
||||
u.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Quota struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
|
||||
ApiKeyID uuid.UUID `gorm:"type:uuid;not null;index" json:"api_key_id"`
|
||||
ModelScope *string `gorm:"size:255" json:"model_scope,omitempty"`
|
||||
PeriodStart time.Time `gorm:"not null;index" json:"period_start"`
|
||||
PeriodEnd time.Time `gorm:"not null" json:"period_end"`
|
||||
TokensLimit int `gorm:"default:0" json:"tokens_limit"`
|
||||
TokensUsed int `gorm:"default:0" json:"tokens_used"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (Quota) TableName() string { return "quotas" }
|
||||
|
||||
func (q *Quota) BeforeCreate(tx *gorm.DB) error {
|
||||
if q.ID == uuid.Nil {
|
||||
q.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Webhook struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
|
||||
ApiKeyID uuid.UUID `gorm:"type:uuid;not null;index" json:"api_key_id"`
|
||||
URL string `gorm:"size:2048;not null" json:"url"`
|
||||
Event string `gorm:"size:100;not null" json:"event"`
|
||||
Secret *string `gorm:"size:255" json:"secret,omitempty"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func (Webhook) TableName() string { return "webhooks" }
|
||||
|
||||
func (w *Webhook) BeforeCreate(tx *gorm.DB) error {
|
||||
if w.ID == uuid.Nil {
|
||||
w.ID = uuid.New()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func SeedAdminKey(db *gorm.DB, adminToken string) error {
|
||||
var count int64
|
||||
db.Model(&ApiKey{}).Count(&count)
|
||||
|
||||
if count > 0 {
|
||||
slog.Info("admin key already exists, skipping seed")
|
||||
return nil
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(adminToken), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
admin := ApiKey{
|
||||
ID: uuid.New(),
|
||||
Name: "admin",
|
||||
KeyHash: string(hash),
|
||||
KeyPrefix: "admin-",
|
||||
Scopes: StringArray{"chat", "models", "usage", "admin"},
|
||||
IsActive: true,
|
||||
IsAdmin: true,
|
||||
}
|
||||
|
||||
if err := db.Create(&admin).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
slog.Info("admin key seeded", "prefix", admin.KeyPrefix)
|
||||
slog.Warn("ADMIN TOKEN: " + adminToken + " (save this!)")
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user