package ai import ( "fmt" "log/slog" "net/http" "strings" "git.warky.dev/wdevs/amcs/internal/ai/compat" "git.warky.dev/wdevs/amcs/internal/config" ) // Registry holds one compat.Client per named provider. Runners look up clients // by provider name when walking a role chain. type Registry struct { clients map[string]*compat.Client } // NewRegistry builds a Registry from the configured providers. Each provider // type maps onto a compat.Client with type-specific header plumbing (e.g. // openrouter's HTTP-Referer / X-Title). func NewRegistry(providers map[string]config.ProviderConfig, httpClient *http.Client, log *slog.Logger) (*Registry, error) { if httpClient == nil { return nil, fmt.Errorf("ai registry: http client is required") } if len(providers) == 0 { return nil, fmt.Errorf("ai registry: no providers configured") } clients := make(map[string]*compat.Client, len(providers)) for name, p := range providers { headers, err := providerHeaders(p) if err != nil { return nil, fmt.Errorf("ai registry: provider %q: %w", name, err) } clients[name] = compat.New(compat.Config{ Name: name, BaseURL: p.BaseURL, APIKey: p.APIKey, Headers: headers, HTTPClient: httpClient, Log: log, }) } return &Registry{clients: clients}, nil } // Client returns the compat.Client registered under name. func (r *Registry) Client(name string) (*compat.Client, error) { c, ok := r.clients[name] if !ok { return nil, fmt.Errorf("ai registry: provider %q is not configured", name) } return c, nil } // Names returns the registered provider names. func (r *Registry) Names() []string { names := make([]string, 0, len(r.clients)) for name := range r.clients { names = append(names, name) } return names } func providerHeaders(p config.ProviderConfig) (map[string]string, error) { switch p.Type { case "litellm", "ollama": return cloneHeaders(p.RequestHeaders), nil case "openrouter": headers := cloneHeaders(p.RequestHeaders) if headers == nil { headers = map[string]string{} } if s := strings.TrimSpace(p.SiteURL); s != "" { headers["HTTP-Referer"] = s } if s := strings.TrimSpace(p.AppName); s != "" { headers["X-Title"] = s } return headers, nil default: return nil, fmt.Errorf("unsupported provider type %q", p.Type) } } func cloneHeaders(in map[string]string) map[string]string { if len(in) == 0 { return nil } out := make(map[string]string, len(in)) for k, v := range in { out[k] = v } return out }