A repeater saved with non-sequential keys was read only at keys 0 to len-1, so a model at a higher key was dropped while the import reported success and refused to run again. Every key is now read, numeric keys in ascending order.
golem15.golem
The shared AI plugin for SummerCMS applications: provider adapters for the Anthropic Messages API and the OpenAI-compatible chat completions API, an AI service that sends prompts through the framework's guarded outbound client, the operator's AI models with their admin screen, and an SSRF guard for untrusted AI URLs.
import _ "git.golem15.com/golem15/sm-golem-plugin"
Module git.golem15.com/golem15/sm-golem-plugin, package golem, plugin ID golem15.golem.
Overview
The plugin is a compiled SummerCMS plugin ported from the WinterCMS Golem15.Golem plugin. Its init registers it with the framework, so an application loads it by listing it in summer.yaml; summer build then emits the blank import above into the application's plugins.gen.go. Another plugin that sends AI requests lists golem15.golem in its Requires() and imports the classes/... packages.
The adapters are hand-written JSON mappings with no vendor SDK. Every request goes through a fetchguard.Client: a config marked Trusted (a model an operator configured) runs in fetchguard.TrustedMode, every other config (a key a user or an organisation supplied) in fetchguard.PublicOnlyMode, which refuses private and reserved addresses at dial time.
The plugin's config keys, lang namespace and plugin ID are a contract shared by every application that mounts it: change them only in a backwards-compatible way.
Features
- Prompts.
valueobjects.Promptis an immutable request: a system prompt, user and assistant messages, messages with base64 images (AddUserMessageWithImage,AddUserMessageWithImages) or an uploaded file id (AddUserMessageWithFileID), a model override and extra options in insertion order.ToAPIPayloadbuilds the OpenAI chat completions body as avalueobjects.Payload, a JSON object that keeps its key order. - Responses.
valueobjects.AIResponsecarries the content, token usage and the provider's decoded answer (Raw, which keepsfinish_reasonandstop_reason) or the failure message.Errholds the Go error behind a failure for logs and tests and is never shown to a user. - OpenAI-compatible adapter.
providers.OpenAIAdapterposts to<base>/chat/completionswithAuthorization: Bearer, dropsresponse_format, mapsmax_tokenstomax_completion_tokensand addsreasoning_effort: lowfor o-series and gpt-5 models. It also serves compatible providers through their base URL.providers.ForNamepicks the adapter by name. - Anthropic adapter.
providers.AnthropicAdapterposts to<base>/messageswithx-api-key,anthropic-version: 2023-06-01andanthropic-beta: files-api-2025-04-14. It sendsmax_tokens(the prompt'smax_tokens, elsemax_completion_tokens, else 4096), rewrites image blocks to base64 or URL image sources and file blocks to document blocks, and maps Anthropic's usage toprompt_tokens,completion_tokensandtotal_tokens.Rawkeepsstop_reason. - AI service.
services.AIService.Sendapplies the model's system prompt when the prompt has none and posts the prompt to the config's chat endpoint; a nil config uses the default model. It returns the WinterCMS plugin's failure messages: no model configured, no API key, no connection, and an answer that is not JSON. The other calls areservices.AIService.SendStream(server-sent events, one{"content":…}chunk per delta),services.AIService.SendFile,services.AIService.SendFilePathandservices.AIService.SendFileReader(multipart upload to the files endpoint of a model withhas_files_endpoint),services.AIService.SendWithModel,services.AIService.SendToImageModel,services.AIService.SendToVisionModel,services.AIService.GenerateImage(the images endpoint; check the returned URL withsecurity.AssertSafeURLbefore fetching it) andservices.AIService.Ask.services.ForAppreturns the application's shared service. - AI models. Operators configure models in the admin under Golem › AI Models (controller
golem15.golem.models, permissiongolem15.golem.access_settings). Each model has a name, an adapter (openaioranthropic), an API key, a base URL, a model id, a default system prompt, flags (enabled, default, generates images, accepts images, has a files endpoint), a completion token budget and an order. The key is encrypted at rest and write-only: the form shows it redacted, and an update that sends it back empty or redacted keeps the stored key, unless the same update changes the adapter or base URL: then the key must be entered again, so a kept key never follows a new endpoint.services.Models,services.DefaultModel,services.VisionModel,services.ImageModel,services.FileModelandservices.ModelByNameread them like the WinterCMS Settings model. Their configs areTrusted.services.DefaultModelkeeps the WinterCMS quirk: without a model marked default it returns the first model only when that one is enabled. - Settings import.
golem:import-settingscopies the AI models of a WinterCMS installation (themodelsrepeater of thesystem_settingsrowgolem_settings, or a JSON file) into the table once, encrypting each plaintext key. - SSRF guard.
security.AssertSafeURLports the WinterCMS SSRFGuard: https only, a host on the allowlist (security.AllowedHosts), and a host that resolves to public addresses only (fetchguard.IsPrivateAddr). A failure is asecurity.UnsafeURLErrorwith the WinterCMS message. Apply it to every base URL a user or an organisation supplies and to every URL a provider returns; the guarded client checks the address again when it connects. - Prompt factory.
factories.PromptFactorybuilds the WinterCMS plugin's ready-made prompts: a form field suggestion, values for several fields, categories for an item, a chat and a completion.
Usage
Mount the plugin in an application as a git submodule at plugins/golem15/golem:
git submodule add git@git.golem15.com:golem15/sm-golem-plugin.git plugins/golem15/golem
Add it to the application's go.work and go.mod:
use ./plugins/golem15/golem
require git.golem15.com/golem15/sm-golem-plugin v0.0.0
replace git.golem15.com/golem15/sm-golem-plugin => ./plugins/golem15/golem
List it in summer.yaml before the plugins that require it and run summer build:
module: git.golem15.com/golem15/acme
binary: acme
plugins:
- id: golem15.golem
module: git.golem15.com/golem15/sm-golem-plugin
- id: acme.blog
module: git.golem15.com/golem15/acme/plugins/acme/blog
The plugin's own go.mod replaces the framework with ../../../../summercms.go, so it expects the framework checkout next to the application checkout.
A plugin sends a prompt with a user's own key:
svc := services.ForApp(app)
prompt := valueobjects.NewPrompt().
WithSystem("You catalog physical items.").
AddUserMessage("List the items.")
res := svc.Send(ctx, prompt, &services.ModelConfig{
Adapter: "openai",
APIKey: key,
Model: "gpt-4o-mini",
})
if !res.Success {
return errors.New(res.Error)
}
API reference
golem:Plugin, thegolem15.golemplugin (config, lang, migrations, models, admin controllers, permissions, navigation, commands).classes/valueobjects:Prompt,NewPrompt,Message,ContentPart,ImageURL,FileRef,Image,Payload,NewPayload,AIResponse,Success,Failure.classes/providers:Adapter,OpenAIAdapter,AnthropicAdapter,ForName,OpenAIBaseURL,AnthropicBaseURL,AnthropicVersion,AnthropicBeta.classes/services:ModelConfig,DefaultModelID,AIService,NewAIService,ForApp,ConfigTimeoutSeconds,DefaultTimeout,Models,DefaultModel,VisionModel,ImageModel,FileModel,ModelByName.classes/security:AssertSafeURL,AllowedHosts,UnsafeURLError,Resolver,DefaultResolver,DefaultAllowedHosts,ConfigAllowedHosts,EnvAllowedHosts.classes/factories:PromptFactory,Pair,FormField,Category,HumanizeFieldName.models:AiModel(tablegolem15_golem_models),Register,All.updates: the migrations (202610030101_create_golem_models),Register,All.controllers:ModelsAdminController,AdminControllers,PermissionAccessSettings.console:ImportSettings,ImportModels,IsNothingToImport,ErrModelsNotEmpty,SettingsItem.
Configuration
Defaults live in config/config.yaml and are merged under golem15.golem.*.
| Key | Default | Meaning |
|---|---|---|
golem15.golem.ssrf.allowed_hosts |
oaidalleapiprodscus.blob.core.windows.net, .openai.com |
Hosts an untrusted base URL or an AI-returned image URL may use. A leading dot matches any subdomain. |
golem15.golem.ai.timeout_seconds |
120 |
Timeout of every AI provider request. |
The environment variable GOLEM15_SSRF_ALLOWED_HOSTS, a comma-separated host list, replaces golem15.golem.ssrf.allowed_hosts when it is set and not empty. With the default list, a user's Anthropic base URL is refused: add .anthropic.com (or the provider's host) to accept it.
CLI commands
| Command | Purpose |
|---|---|
golem:import-settings [--file <json>] |
Copy the AI models of a WinterCMS golem_settings row (or of a JSON file holding the settings value or the models list) into golem15_golem_models, in order, with each key encrypted. It refuses to run when the table already has rows, and reports a missing system_settings table or row without failing. |
Dependencies
- The SummerCMS framework,
git.golem15.com/golem15/summercms(modulesbackpack,bonfire,cabana,compass,fetchguard,lagoon,pact,party). - GORM and gormigrate, as pinned in
go.mod. - testcontainers-go for the Postgres-backed tests.
Testing
go test ./... # starts Postgres with testcontainers (needs Docker)