- 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
118 lines
3.4 KiB
Go
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 ""
|
|
}
|