Files
sm-golem-plugin/classes/services/settings.go
Jakub Zych c93e2d9142 feat: AI models admin, settings import, Anthropic adapter and the rest of AIService
- golem15_golem_models (encrypted write-only api_key) with its cabana list
  and form under golem15.golem.access_settings, and the Settings lookups
  (Models, DefaultModel with PHP's position-0 quirk, Vision, Image, File,
  ByName), every one Trusted
- golem:import-settings copies the WinterCMS golem_settings repeater once
- AnthropicAdapter; SendStream, SendFile/SendFilePath/SendFileReader,
  SendWithModel, SendToImageModel, SendToVisionModel, GenerateImage, Ask
- PromptFactory; README documents the config keys, the environment
  variable, the command and the admin screen
2026-10-03 23:09:02 +02:00

120 lines
3.4 KiB
Go

package services
import (
"context"
"git.golem15.com/golem15/sm-golem-plugin/models"
"gorm.io/gorm"
)
// The functions below port the WinterCMS plugin's Settings model over the
// golem15_golem_models rows (in sort_order, then id order). Every config
// they return is an operator's model: Trusted is set. A nil db means no
// model is configured.
// Models is Settings::getModels: the enabled models, in order. Each keeps
// its Position among all models, enabled or not, as PHP's array_filter
// keeps the repeater keys.
func Models(ctx context.Context, db *gorm.DB) ([]*ModelConfig, error) {
if db == nil {
return nil, nil
}
var rows []models.AiModel
if err := db.WithContext(ctx).Order("sort_order, id").Find(&rows).Error; err != nil {
return nil, err
}
var out []*ModelConfig
for i := range rows {
if !rows[i].IsEnabled {
continue
}
c := modelConfigFrom(&rows[i])
c.Position = i
out = append(out, c)
}
return out, nil
}
// DefaultModel is Settings::getDefaultModel: the first enabled model
// marked default, else the model at position 0 when it is enabled. PHP's
// `$models[0] ?? null` reads the filtered array by key, so a disabled
// first model means no default even when later models are enabled.
func DefaultModel(ctx context.Context, db *gorm.DB) (*ModelConfig, error) {
all, err := Models(ctx, db)
if err != nil {
return nil, err
}
for _, m := range all {
if m.isDefault {
return m, nil
}
}
for _, m := range all {
if m.Position == 0 {
return m, nil
}
}
return nil, nil
}
// VisionModel is Settings::getVisionModel: the first enabled model that
// accepts images.
func VisionModel(ctx context.Context, db *gorm.DB) (*ModelConfig, error) {
return firstModel(ctx, db, func(m *ModelConfig) bool { return m.AcceptsImages })
}
// ImageModel is Settings::getImageModel: the first enabled model that
// generates images.
func ImageModel(ctx context.Context, db *gorm.DB) (*ModelConfig, error) {
return firstModel(ctx, db, func(m *ModelConfig) bool { return m.GeneratesImages })
}
// FileModel is Settings::getFileModel: the first enabled model with a
// files endpoint.
func FileModel(ctx context.Context, db *gorm.DB) (*ModelConfig, error) {
return firstModel(ctx, db, func(m *ModelConfig) bool { return m.HasFilesEndpoint })
}
// ModelByName is Settings::getModelByName: the first enabled model named
// name.
func ModelByName(ctx context.Context, db *gorm.DB, name string) (*ModelConfig, error) {
return firstModel(ctx, db, func(m *ModelConfig) bool { return m.Name == name })
}
func firstModel(ctx context.Context, db *gorm.DB, match func(*ModelConfig) bool) (*ModelConfig, error) {
all, err := Models(ctx, db)
if err != nil {
return nil, err
}
for _, m := range all {
if match(m) {
return m, nil
}
}
return nil, nil
}
func modelConfigFrom(row *models.AiModel) *ModelConfig {
c := &ModelConfig{
Name: row.Name,
Adapter: row.Adapter,
APIKey: row.APIKey.Reveal(),
Model: row.Model,
HasFilesEndpoint: row.HasFilesEndpoint,
AcceptsImages: row.AcceptsImages,
GeneratesImages: row.GeneratesImages,
Trusted: true,
isDefault: row.IsDefault,
}
if row.BaseURL != nil {
c.BaseURL = *row.BaseURL
}
if row.SystemPrompt != nil {
c.SystemPrompt = *row.SystemPrompt
}
if row.MaxCompletionTokens != nil {
c.MaxCompletionTokens = *row.MaxCompletionTokens
}
return c
}