# 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. ```go 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.Prompt` is 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. `ToAPIPayload` builds the OpenAI chat completions body as a `valueobjects.Payload`, a JSON object that keeps its key order. - **Responses.** `valueobjects.AIResponse` carries the content, token usage and the provider's decoded answer (`Raw`, which keeps `finish_reason` and `stop_reason`) or the failure message. `Err` holds the Go error behind a failure for logs and tests and is never shown to a user. - **OpenAI-compatible adapter.** `providers.OpenAIAdapter` posts to `/chat/completions` with `Authorization: Bearer`, drops `response_format`, maps `max_tokens` to `max_completion_tokens` and adds `reasoning_effort: low` for o-series and gpt-5 models. It also serves compatible providers through their base URL. `providers.ForName` picks the adapter by name. - **Anthropic adapter.** `providers.AnthropicAdapter` posts to `/messages` with `x-api-key`, `anthropic-version: 2023-06-01` and `anthropic-beta: files-api-2025-04-14`. It sends `max_tokens` (the prompt's `max_tokens`, else `max_completion_tokens`, else 4096), rewrites image blocks to base64 or URL image sources and file blocks to document blocks, and maps Anthropic's usage to `prompt_tokens`, `completion_tokens` and `total_tokens`. `Raw` keeps `stop_reason`. - **AI service.** `services.AIService.Send` applies 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 are `services.AIService.SendStream` (server-sent events, one `{"content":…}` chunk per delta), `services.AIService.SendFile`, `services.AIService.SendFilePath` and `services.AIService.SendFileReader` (multipart upload to the files endpoint of a model with `has_files_endpoint`), `services.AIService.SendWithModel`, `services.AIService.SendToImageModel`, `services.AIService.SendToVisionModel`, `services.AIService.GenerateImage` (the images endpoint; check the returned URL with `security.AssertSafeURL` before fetching it) and `services.AIService.Ask`. `services.ForApp` returns the application's shared service. - **AI models.** Operators configure models in the admin under Golem › AI Models (controller `golem15.golem.models`, permission `golem15.golem.access_settings`). Each model has a name, an adapter (`openai` or `anthropic`), 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.FileModel` and `services.ModelByName` read them like the WinterCMS Settings model. Their configs are `Trusted`. `services.DefaultModel` keeps the WinterCMS quirk: without a model marked default it returns the first model only when that one is enabled. - **Settings import.** `golem:import-settings` copies the AI models of a WinterCMS installation (the `models` repeater of the `system_settings` row `golem_settings`, or a JSON file) into the table once, encrypting each plaintext key. - **SSRF guard.** `security.AssertSafeURL` ports 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 a `security.UnsafeURLError` with 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.PromptFactory` builds 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`: ```sh 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`: ```text 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`: ```yaml 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: ```go 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`, the `golem15.golem` plugin (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` (table `golem15_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 ]` | 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` (modules `backpack`, `bonfire`, `cabana`, `compass`, `fetchguard`, `lagoon`, `pact`, `party`). - GORM and gormigrate, as pinned in `go.mod`. - testcontainers-go for the Postgres-backed tests. ## Testing ```sh go test ./... # starts Postgres with testcontainers (needs Docker) ```