// 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 "" }