refactor(sdk/auth): rename manager.go to conductor.go

This commit is contained in:
hkfires
2025-12-24 15:21:03 +08:00
parent 5dc0dbc7aa
commit 26fbb77901
5 changed files with 118 additions and 118 deletions

View File

@@ -279,26 +279,26 @@ func (m *AmpModule) hasModelMappingsChanged(old *config.AmpCode, new *config.Amp
return true return true
} }
// Build map for efficient and robust comparison // Build map for efficient and robust comparison
type mappingInfo struct { type mappingInfo struct {
to string to string
regex bool regex bool
} }
oldMap := make(map[string]mappingInfo, len(old.ModelMappings)) oldMap := make(map[string]mappingInfo, len(old.ModelMappings))
for _, mapping := range old.ModelMappings { for _, mapping := range old.ModelMappings {
oldMap[strings.TrimSpace(mapping.From)] = mappingInfo{ oldMap[strings.TrimSpace(mapping.From)] = mappingInfo{
to: strings.TrimSpace(mapping.To), to: strings.TrimSpace(mapping.To),
regex: mapping.Regex, regex: mapping.Regex,
} }
} }
for _, mapping := range new.ModelMappings { for _, mapping := range new.ModelMappings {
from := strings.TrimSpace(mapping.From) from := strings.TrimSpace(mapping.From)
to := strings.TrimSpace(mapping.To) to := strings.TrimSpace(mapping.To)
if oldVal, exists := oldMap[from]; !exists || oldVal.to != to || oldVal.regex != mapping.Regex { if oldVal, exists := oldMap[from]; !exists || oldVal.to != to || oldVal.regex != mapping.Regex {
return true return true
} }
} }
return false return false
} }

View File

@@ -3,7 +3,7 @@
package amp package amp
import ( import (
"regexp" "regexp"
"strings" "strings"
"sync" "sync"
@@ -27,15 +27,15 @@ type ModelMapper interface {
// DefaultModelMapper implements ModelMapper with thread-safe mapping storage. // DefaultModelMapper implements ModelMapper with thread-safe mapping storage.
type DefaultModelMapper struct { type DefaultModelMapper struct {
mu sync.RWMutex mu sync.RWMutex
mappings map[string]string // exact: from -> to (normalized lowercase keys) mappings map[string]string // exact: from -> to (normalized lowercase keys)
regexps []regexMapping // regex rules evaluated in order regexps []regexMapping // regex rules evaluated in order
} }
// NewModelMapper creates a new model mapper with the given initial mappings. // NewModelMapper creates a new model mapper with the given initial mappings.
func NewModelMapper(mappings []config.AmpModelMapping) *DefaultModelMapper { func NewModelMapper(mappings []config.AmpModelMapping) *DefaultModelMapper {
m := &DefaultModelMapper{ m := &DefaultModelMapper{
mappings: make(map[string]string), mappings: make(map[string]string),
regexps: nil, regexps: nil,
} }
m.UpdateMappings(mappings) m.UpdateMappings(mappings)
return m return m
@@ -58,18 +58,18 @@ func (m *DefaultModelMapper) MapModel(requestedModel string) string {
// Check for direct mapping // Check for direct mapping
targetModel, exists := m.mappings[normalizedRequest] targetModel, exists := m.mappings[normalizedRequest]
if !exists { if !exists {
// Try regex mappings in order // Try regex mappings in order
base, _ := util.NormalizeThinkingModel(requestedModel) base, _ := util.NormalizeThinkingModel(requestedModel)
for _, rm := range m.regexps { for _, rm := range m.regexps {
if rm.re.MatchString(requestedModel) || (base != "" && rm.re.MatchString(base)) { if rm.re.MatchString(requestedModel) || (base != "" && rm.re.MatchString(base)) {
targetModel = rm.to targetModel = rm.to
exists = true exists = true
break break
} }
} }
if !exists { if !exists {
return "" return ""
} }
} }
// Verify target model has available providers // Verify target model has available providers
@@ -91,8 +91,8 @@ func (m *DefaultModelMapper) UpdateMappings(mappings []config.AmpModelMapping) {
defer m.mu.Unlock() defer m.mu.Unlock()
// Clear and rebuild mappings // Clear and rebuild mappings
m.mappings = make(map[string]string, len(mappings)) m.mappings = make(map[string]string, len(mappings))
m.regexps = make([]regexMapping, 0, len(mappings)) m.regexps = make([]regexMapping, 0, len(mappings))
for _, mapping := range mappings { for _, mapping := range mappings {
from := strings.TrimSpace(mapping.From) from := strings.TrimSpace(mapping.From)
@@ -103,30 +103,30 @@ func (m *DefaultModelMapper) UpdateMappings(mappings []config.AmpModelMapping) {
continue continue
} }
if mapping.Regex { if mapping.Regex {
// Compile case-insensitive regex; wrap with (?i) to match behavior of exact lookups // Compile case-insensitive regex; wrap with (?i) to match behavior of exact lookups
pattern := "(?i)" + from pattern := "(?i)" + from
re, err := regexp.Compile(pattern) re, err := regexp.Compile(pattern)
if err != nil { if err != nil {
log.Warnf("amp model mapping: invalid regex %q: %v", from, err) log.Warnf("amp model mapping: invalid regex %q: %v", from, err)
continue continue
} }
m.regexps = append(m.regexps, regexMapping{re: re, to: to}) m.regexps = append(m.regexps, regexMapping{re: re, to: to})
log.Debugf("amp model regex mapping registered: /%s/ -> %s", from, to) log.Debugf("amp model regex mapping registered: /%s/ -> %s", from, to)
} else { } else {
// Store with normalized lowercase key for case-insensitive lookup // Store with normalized lowercase key for case-insensitive lookup
normalizedFrom := strings.ToLower(from) normalizedFrom := strings.ToLower(from)
m.mappings[normalizedFrom] = to m.mappings[normalizedFrom] = to
log.Debugf("amp model mapping registered: %s -> %s", from, to) log.Debugf("amp model mapping registered: %s -> %s", from, to)
} }
} }
if len(m.mappings) > 0 { if len(m.mappings) > 0 {
log.Infof("amp model mapping: loaded %d mapping(s)", len(m.mappings)) log.Infof("amp model mapping: loaded %d mapping(s)", len(m.mappings))
} }
if n := len(m.regexps); n > 0 { if n := len(m.regexps); n > 0 {
log.Infof("amp model mapping: loaded %d regex mapping(s)", n) log.Infof("amp model mapping: loaded %d regex mapping(s)", n)
} }
} }
// GetMappings returns a copy of current mappings (for debugging/status). // GetMappings returns a copy of current mappings (for debugging/status).
@@ -142,6 +142,6 @@ func (m *DefaultModelMapper) GetMappings() map[string]string {
} }
type regexMapping struct { type regexMapping struct {
re *regexp.Regexp re *regexp.Regexp
to string to string
} }

View File

@@ -205,79 +205,79 @@ func TestModelMapper_GetMappings_ReturnsCopy(t *testing.T) {
} }
func TestModelMapper_Regex_MatchBaseWithoutParens(t *testing.T) { func TestModelMapper_Regex_MatchBaseWithoutParens(t *testing.T) {
reg := registry.GetGlobalRegistry() reg := registry.GetGlobalRegistry()
reg.RegisterClient("test-client-regex-1", "gemini", []*registry.ModelInfo{ reg.RegisterClient("test-client-regex-1", "gemini", []*registry.ModelInfo{
{ID: "gemini-2.5-pro", OwnedBy: "google", Type: "gemini"}, {ID: "gemini-2.5-pro", OwnedBy: "google", Type: "gemini"},
}) })
defer reg.UnregisterClient("test-client-regex-1") defer reg.UnregisterClient("test-client-regex-1")
mappings := []config.AmpModelMapping{ mappings := []config.AmpModelMapping{
{From: "^gpt-5$", To: "gemini-2.5-pro", Regex: true}, {From: "^gpt-5$", To: "gemini-2.5-pro", Regex: true},
} }
mapper := NewModelMapper(mappings) mapper := NewModelMapper(mappings)
// Incoming model has reasoning suffix but should match base via regex // Incoming model has reasoning suffix but should match base via regex
result := mapper.MapModel("gpt-5(high)") result := mapper.MapModel("gpt-5(high)")
if result != "gemini-2.5-pro" { if result != "gemini-2.5-pro" {
t.Errorf("Expected gemini-2.5-pro, got %s", result) t.Errorf("Expected gemini-2.5-pro, got %s", result)
} }
} }
func TestModelMapper_Regex_ExactPrecedence(t *testing.T) { func TestModelMapper_Regex_ExactPrecedence(t *testing.T) {
reg := registry.GetGlobalRegistry() reg := registry.GetGlobalRegistry()
reg.RegisterClient("test-client-regex-2", "claude", []*registry.ModelInfo{ reg.RegisterClient("test-client-regex-2", "claude", []*registry.ModelInfo{
{ID: "claude-sonnet-4", OwnedBy: "anthropic", Type: "claude"}, {ID: "claude-sonnet-4", OwnedBy: "anthropic", Type: "claude"},
}) })
reg.RegisterClient("test-client-regex-3", "gemini", []*registry.ModelInfo{ reg.RegisterClient("test-client-regex-3", "gemini", []*registry.ModelInfo{
{ID: "gemini-2.5-pro", OwnedBy: "google", Type: "gemini"}, {ID: "gemini-2.5-pro", OwnedBy: "google", Type: "gemini"},
}) })
defer reg.UnregisterClient("test-client-regex-2") defer reg.UnregisterClient("test-client-regex-2")
defer reg.UnregisterClient("test-client-regex-3") defer reg.UnregisterClient("test-client-regex-3")
mappings := []config.AmpModelMapping{ mappings := []config.AmpModelMapping{
{From: "gpt-5", To: "claude-sonnet-4"}, // exact {From: "gpt-5", To: "claude-sonnet-4"}, // exact
{From: "^gpt-5.*$", To: "gemini-2.5-pro", Regex: true}, // regex {From: "^gpt-5.*$", To: "gemini-2.5-pro", Regex: true}, // regex
} }
mapper := NewModelMapper(mappings) mapper := NewModelMapper(mappings)
// Exact match should win over regex // Exact match should win over regex
result := mapper.MapModel("gpt-5") result := mapper.MapModel("gpt-5")
if result != "claude-sonnet-4" { if result != "claude-sonnet-4" {
t.Errorf("Expected claude-sonnet-4, got %s", result) t.Errorf("Expected claude-sonnet-4, got %s", result)
} }
} }
func TestModelMapper_Regex_InvalidPattern_Skipped(t *testing.T) { func TestModelMapper_Regex_InvalidPattern_Skipped(t *testing.T) {
// Invalid regex should be skipped and not cause panic // Invalid regex should be skipped and not cause panic
mappings := []config.AmpModelMapping{ mappings := []config.AmpModelMapping{
{From: "(", To: "target", Regex: true}, {From: "(", To: "target", Regex: true},
} }
mapper := NewModelMapper(mappings) mapper := NewModelMapper(mappings)
result := mapper.MapModel("anything") result := mapper.MapModel("anything")
if result != "" { if result != "" {
t.Errorf("Expected empty result due to invalid regex, got %s", result) t.Errorf("Expected empty result due to invalid regex, got %s", result)
} }
} }
func TestModelMapper_Regex_CaseInsensitive(t *testing.T) { func TestModelMapper_Regex_CaseInsensitive(t *testing.T) {
reg := registry.GetGlobalRegistry() reg := registry.GetGlobalRegistry()
reg.RegisterClient("test-client-regex-4", "claude", []*registry.ModelInfo{ reg.RegisterClient("test-client-regex-4", "claude", []*registry.ModelInfo{
{ID: "claude-sonnet-4", OwnedBy: "anthropic", Type: "claude"}, {ID: "claude-sonnet-4", OwnedBy: "anthropic", Type: "claude"},
}) })
defer reg.UnregisterClient("test-client-regex-4") defer reg.UnregisterClient("test-client-regex-4")
mappings := []config.AmpModelMapping{ mappings := []config.AmpModelMapping{
{From: "^CLAUDE-OPUS-.*$", To: "claude-sonnet-4", Regex: true}, {From: "^CLAUDE-OPUS-.*$", To: "claude-sonnet-4", Regex: true},
} }
mapper := NewModelMapper(mappings) mapper := NewModelMapper(mappings)
result := mapper.MapModel("claude-opus-4.5") result := mapper.MapModel("claude-opus-4.5")
if result != "claude-sonnet-4" { if result != "claude-sonnet-4" {
t.Errorf("Expected claude-sonnet-4, got %s", result) t.Errorf("Expected claude-sonnet-4, got %s", result)
} }
} }

View File

@@ -145,10 +145,10 @@ type AmpModelMapping struct {
// The target model must have available providers in the registry. // The target model must have available providers in the registry.
To string `yaml:"to" json:"to"` To string `yaml:"to" json:"to"`
// Regex indicates whether the 'from' field should be interpreted as a regular // Regex indicates whether the 'from' field should be interpreted as a regular
// expression for matching model names. When true, this mapping is evaluated // expression for matching model names. When true, this mapping is evaluated
// after exact matches and in the order provided. Defaults to false (exact match). // after exact matches and in the order provided. Defaults to false (exact match).
Regex bool `yaml:"regex,omitempty" json:"regex,omitempty"` Regex bool `yaml:"regex,omitempty" json:"regex,omitempty"`
} }
// AmpCode groups Amp CLI integration settings including upstream routing, // AmpCode groups Amp CLI integration settings including upstream routing,