mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-15 08:17:09 +00:00
154 lines
5.0 KiB
Go
154 lines
5.0 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
// RegistryResolver adapts providers.Registry + store.SystemConfigStore into
|
|
// the ProviderResolver interface consumed by PromptHandler. It applies a
|
|
// simple fallback chain:
|
|
//
|
|
// 1. Explicit provider + model from the hook config.
|
|
// 2. Legacy explicit model alias → map to provider by convention.
|
|
// 3. System config `hooks.prompt.provider` / `hooks.prompt.model`.
|
|
// 4. System config `background.provider` / `background.model`.
|
|
// 5. First registered provider for the tenant.
|
|
//
|
|
// Keeping this adapter in `handlers` rather than the higher-level
|
|
// `providerresolve` package avoids a new import cycle (providerresolve →
|
|
// store → hooks would introduce a diamond).
|
|
type RegistryResolver struct {
|
|
Registry *providers.Registry
|
|
SysConfig store.SystemConfigStore
|
|
DefaultProviderForAlias func(alias string) string
|
|
}
|
|
|
|
// NewRegistryResolver returns a RegistryResolver with sensible defaults.
|
|
// registry MUST be non-nil. sysConfig may be nil (fallback to step 4 only).
|
|
func NewRegistryResolver(registry *providers.Registry, sysConfig store.SystemConfigStore) *RegistryResolver {
|
|
return &RegistryResolver{
|
|
Registry: registry,
|
|
SysConfig: sysConfig,
|
|
DefaultProviderForAlias: defaultProviderForAlias,
|
|
}
|
|
}
|
|
|
|
// ResolveForHook implements ProviderResolver.
|
|
func (r *RegistryResolver) ResolveForHook(ctx context.Context, tenantID uuid.UUID, preferredProvider, preferredModel string) (providers.Provider, string, error) {
|
|
if r == nil || r.Registry == nil {
|
|
return nil, "", errors.New("hook resolver: nil registry")
|
|
}
|
|
|
|
// Step 1: an explicit provider is authoritative. Never silently route its
|
|
// model to another provider when a hook is misconfigured.
|
|
if providerName := strings.TrimSpace(preferredProvider); providerName != "" {
|
|
p, err := r.Registry.GetForTenant(tenantID, providerName)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("hook resolver: configured provider %q: %w", providerName, err)
|
|
}
|
|
if p == nil {
|
|
return nil, "", fmt.Errorf("hook resolver: configured provider %q returned nil", providerName)
|
|
}
|
|
model := strings.TrimSpace(preferredModel)
|
|
if model == "" {
|
|
model = p.DefaultModel()
|
|
}
|
|
return p, model, nil
|
|
}
|
|
|
|
// Step 2: legacy explicit alias → provider name.
|
|
if preferredModel != "" {
|
|
if name := r.providerForAlias(preferredModel); name != "" {
|
|
if p, err := r.Registry.GetForTenant(tenantID, name); err == nil && p != nil {
|
|
return p, preferredModel, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 3: system config hooks.prompt.*
|
|
configs := r.loadConfigs(ctx, tenantID)
|
|
if p, m, ok := r.tryConfig(tenantID, configs["hooks.prompt.provider"], configs["hooks.prompt.model"], preferredModel); ok {
|
|
return p, m, nil
|
|
}
|
|
|
|
// Step 4: fall back to background.*
|
|
if p, m, ok := r.tryConfig(tenantID, configs["background.provider"], configs["background.model"], preferredModel); ok {
|
|
return p, m, nil
|
|
}
|
|
|
|
// Step 5: first registered provider for the tenant. This path remains only
|
|
// for backward compatibility with hooks that predate explicit providers.
|
|
names := r.Registry.ListForTenant(tenantID)
|
|
if len(names) == 0 {
|
|
return nil, "", errors.New("hook resolver: no providers registered for tenant")
|
|
}
|
|
p, err := r.Registry.GetForTenant(tenantID, names[0])
|
|
if err != nil || p == nil {
|
|
return nil, "", err
|
|
}
|
|
model := preferredModel
|
|
if model == "" {
|
|
model = p.DefaultModel()
|
|
}
|
|
return p, model, nil
|
|
}
|
|
|
|
func (r *RegistryResolver) providerForAlias(alias string) string {
|
|
if r.DefaultProviderForAlias == nil {
|
|
return defaultProviderForAlias(alias)
|
|
}
|
|
return r.DefaultProviderForAlias(alias)
|
|
}
|
|
|
|
func (r *RegistryResolver) loadConfigs(ctx context.Context, tenantID uuid.UUID) map[string]string {
|
|
if r.SysConfig == nil {
|
|
return nil
|
|
}
|
|
tctx := store.WithTenantID(ctx, tenantID)
|
|
configs, _ := r.SysConfig.List(tctx)
|
|
return configs
|
|
}
|
|
|
|
func (r *RegistryResolver) tryConfig(tenantID uuid.UUID, name, cfgModel, preferred string) (providers.Provider, string, bool) {
|
|
if name == "" {
|
|
return nil, "", false
|
|
}
|
|
p, err := r.Registry.GetForTenant(tenantID, name)
|
|
if err != nil || p == nil {
|
|
return nil, "", false
|
|
}
|
|
model := preferred
|
|
if model == "" {
|
|
model = cfgModel
|
|
}
|
|
if model == "" {
|
|
model = p.DefaultModel()
|
|
}
|
|
return p, model, true
|
|
}
|
|
|
|
// defaultProviderForAlias maps short model aliases to provider names.
|
|
// Unknown aliases return "" → resolver falls through to system-config step.
|
|
func defaultProviderForAlias(alias string) string {
|
|
a := strings.ToLower(strings.TrimSpace(alias))
|
|
switch {
|
|
case strings.HasPrefix(a, "claude"), a == "haiku", a == "sonnet", a == "opus":
|
|
return "anthropic"
|
|
case strings.HasPrefix(a, "gpt"), strings.HasPrefix(a, "o1"), strings.HasPrefix(a, "o3"):
|
|
return "openai"
|
|
case strings.HasPrefix(a, "gemini"):
|
|
return "google"
|
|
case strings.HasPrefix(a, "qwen"):
|
|
return "dashscope"
|
|
}
|
|
return ""
|
|
}
|