Files
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

118 lines
3.4 KiB
Go

// Package providers maps AI requests and answers to each provider's wire
// format: the OpenAI-compatible chat completions API and the Anthropic
// Messages API. The adapters only build and parse JSON; the AI service in
// classes/services sends the requests through the framework's guarded
// outbound client.
package providers
import (
"net/http"
"strconv"
"strings"
"git.golem15.com/golem15/sm-golem-plugin/classes/valueobjects"
"git.golem15.com/golem15/summercms/modules/fetchguard"
)
// Adapter is one provider's mapping (PHP AIProviderAdapter).
type Adapter interface {
// Headers are the provider's auth and version headers for apiKey.
Headers(apiKey string) http.Header
// ChatEndpoint is the chat URL under baseURL ("" means the
// provider's default base URL).
ChatEndpoint(baseURL string) string
// FilesEndpoint is the file upload URL under baseURL.
FilesEndpoint(baseURL string) string
// ImageEndpoint is the image generation URL, "" when the provider
// has none.
ImageEndpoint(baseURL string) string
// BuildChatPayload is the chat request body for prompt, with modelID
// unless the prompt overrides the model.
BuildChatPayload(prompt valueobjects.Prompt, modelID string) *valueobjects.Payload
// FileUploadFields are the plain multipart fields of a file upload.
FileUploadFields(purpose string) []fetchguard.FormField
// FileUploadHeaders are the headers of a file upload.
FileUploadHeaders(apiKey string) http.Header
// ParseChatResponse maps a decoded chat answer to an AIResponse.
ParseChatResponse(data map[string]any) valueobjects.AIResponse
// ParseStreamChunk reads one server-sent-events line: the content
// delta it carries ("" when none) and whether the stream ended.
ParseStreamChunk(line string) (delta string, done bool)
}
// Default base URLs, used when a config leaves base_url empty.
const (
OpenAIBaseURL = "https://api.openai.com/v1"
AnthropicBaseURL = "https://api.anthropic.com/v1"
)
// ForName is ProviderFactory::make: the Anthropic adapter for "anthropic",
// the OpenAI-compatible adapter for anything else.
func ForName(adapter string) Adapter {
if adapter == "anthropic" {
return AnthropicAdapter{}
}
return OpenAIAdapter{}
}
// endpoint is rtrim(base, '/') . path, with def standing in for an empty
// base.
func endpoint(base, def, path string) string {
if base == "" {
base = def
}
return strings.TrimRight(base, "/") + path
}
// dig walks nested JSON objects and arrays (PHP $a['x'][0]['y'] ?? null).
func dig(v any, path ...any) any {
for _, p := range path {
switch key := p.(type) {
case string:
m, ok := v.(map[string]any)
if !ok {
return nil
}
v = m[key]
case int:
s, ok := v.([]any)
if !ok || key < 0 || key >= len(s) {
return nil
}
v = s[key]
default:
return nil
}
}
return v
}
// errorMessage is PHP's `$data['error']['message'] ?? 'Unknown API
// error'` for a response that carries an error key, and false otherwise.
func errorMessage(data map[string]any) (string, bool) {
e, ok := data["error"]
if !ok || e == nil {
return "", false
}
if msg := dig(e, "message"); msg != nil {
return phpString(msg), true
}
return "Unknown API error", true
}
// phpString is a PHP string cast of a decoded JSON scalar.
func phpString(v any) string {
switch t := v.(type) {
case string:
return t
case bool:
if t {
return "1"
}
return ""
case float64:
return strconv.FormatFloat(t, 'f', -1, 64)
}
return ""
}