152 lines
3.7 KiB
Go
152 lines
3.7 KiB
Go
|
|
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
|
|||
|
|
}
|