thread-master/apps/backend/internal/module/ai/registry.go

152 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package ai
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"sync"
"time"
)
const (
ProviderXAI = "xai"
ProviderOpenCodeGo = "opencode-go"
// ModelsCacheTTL — list models cache window
ModelsCacheTTL = 5 * time.Minute
)
// Registry holds provider clients + models cache.
type Registry struct {
clients map[string]Client
mu sync.Mutex
cache map[string]modelsCacheEntry
}
type modelsCacheEntry struct {
Models []string
ExpiresAt time.Time
}
func NewRegistry() *Registry {
return &Registry{
clients: map[string]Client{
ProviderXAI: NewOpenAICompatible(ProviderXAI, "https://api.x.ai/v1"),
ProviderOpenCodeGo: NewOpenAICompatible(ProviderOpenCodeGo, "https://opencode.ai/zen/go/v1"),
},
cache: map[string]modelsCacheEntry{},
}
}
func NormalizeProvider(p string) string {
switch strings.ToLower(strings.TrimSpace(p)) {
case "opencode-go", "opencode", "opencode_go":
return ProviderOpenCodeGo
case "xai", "grok", "":
return ProviderXAI
default:
return strings.ToLower(strings.TrimSpace(p))
}
}
func (r *Registry) Client(provider string) (Client, error) {
id := NormalizeProvider(provider)
c, ok := r.clients[id]
if !ok {
return nil, fmt.Errorf("unsupported provider: %s (use xai or opencode-go)", provider)
}
return c, nil
}
// ListModelsCached fetches models via provider API; caches 5 minutes per provider+key.
// fromCache indicates whether result was served from cache.
func (r *Registry) ListModelsCached(ctx context.Context, provider, apiKey string) (models []string, fromCache bool, err error) {
id := NormalizeProvider(provider)
key := cacheKey(id, apiKey)
r.mu.Lock()
if ent, ok := r.cache[key]; ok && time.Now().Before(ent.ExpiresAt) {
out := append([]string(nil), ent.Models...)
r.mu.Unlock()
return out, true, nil
}
r.mu.Unlock()
c, err := r.Client(id)
if err != nil {
return nil, false, err
}
list, err := c.ListModels(ctx, apiKey)
if err != nil {
return nil, false, err
}
r.mu.Lock()
r.cache[key] = modelsCacheEntry{
Models: append([]string(nil), list...),
ExpiresAt: time.Now().Add(ModelsCacheTTL),
}
r.mu.Unlock()
return list, false, nil
}
func cacheKey(provider, apiKey string) string {
sum := sha256.Sum256([]byte(provider + "\n" + apiKey))
return hex.EncodeToString(sum[:16])
}
// FallbackModels when no key / provider error (never empty for UI).
// OpenCode Go 模型 id 見 https://opencode.ai/docs/go/(不是 OpenAI gpt-*
func FallbackModels(provider string) []string {
switch NormalizeProvider(provider) {
case ProviderOpenCodeGo:
return []string{
"kimi-k2.6",
"glm-5.1",
"glm-5.2",
"deepseek-v4-flash",
"deepseek-v4-pro",
"qwen3.7-plus",
"minimax-m2.7",
"mimo-v2.5",
}
default:
return []string{"grok-3", "grok-3-mini", "grok-2"}
}
}
// DefaultModel for provider when member settings model empty / invalid.
func DefaultModel(provider string) string {
list := FallbackModels(provider)
if len(list) == 0 {
return "grok-3"
}
return list[0]
}
// SanitizeModel remaps invalid model ids for a provider (e.g. gpt-4o-mini on opencode-go).
func SanitizeModel(provider, model string) string {
provider = NormalizeProvider(provider)
model = strings.TrimSpace(model)
if model == "" {
return DefaultModel(provider)
}
if provider != ProviderOpenCodeGo {
// xAI允許 grok-*;若誤存 opencode 模型名,仍交由 API 驗證
return model
}
// OpenCode Go常見錯誤 — 從 xAI / OpenAI 帶過來的模型名
lower := strings.ToLower(model)
if strings.HasPrefix(lower, "gpt-") ||
strings.HasPrefix(lower, "o1") ||
strings.HasPrefix(lower, "o3") ||
strings.HasPrefix(lower, "grok") ||
strings.HasPrefix(lower, "claude") ||
strings.HasPrefix(lower, "gemini") {
return DefaultModel(provider)
}
return model
}