freebuff2api / models.go
xingshang3084's picture
Deploy Freebuff2API Docker Space
c0adbd0 verified
Raw
History Blame Contribute Delete
6.15 kB
package main
import (
"context"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"regexp"
"sort"
"strings"
"sync"
"time"
)
const (
freeAgentsSourceURL = "https://raw.githubusercontent.com/CodebuffAI/codebuff/main/common/src/constants/free-agents.ts"
modelRefreshInterval = 6 * time.Hour
)
// hardcodedFallback is used when the remote fetch fails on startup.
var hardcodedFallback = map[string][]string{
"base2-free": {"minimax/minimax-m2.7", "z-ai/glm-5.1"},
"file-picker": {"google/gemini-2.5-flash-lite"},
"file-picker-max": {"google/gemini-3.1-flash-lite-preview"},
"file-lister": {"google/gemini-3.1-flash-lite-preview"},
"researcher-web": {"google/gemini-3.1-flash-lite-preview"},
"researcher-docs": {"google/gemini-3.1-flash-lite-preview"},
"basher": {"google/gemini-3.1-flash-lite-preview"},
"editor-lite": {"minimax/minimax-m2.7", "z-ai/glm-5.1"},
"code-reviewer-lite": {"minimax/minimax-m2.7", "z-ai/glm-5.1"},
}
// ModelRegistry fetches and caches the agent→model mapping for all free agents
// from the upstream free-agents.ts source file.
type ModelRegistry struct {
client *http.Client
logger *log.Logger
mu sync.RWMutex
agentModels map[string][]string // agentID → []model
modelToAgent map[string]string // model → chosen agentID
allModels []string // deduplicated, sorted
lastOK time.Time
stopCh chan struct{}
wg sync.WaitGroup
}
func NewModelRegistry(client *http.Client, logger *log.Logger) *ModelRegistry {
return &ModelRegistry{
client: client,
logger: logger,
agentModels: make(map[string][]string),
modelToAgent: make(map[string]string),
stopCh: make(chan struct{}),
}
}
func (r *ModelRegistry) Start(ctx context.Context) {
if err := r.refresh(ctx); err != nil {
r.logger.Printf("model registry: initial fetch failed, loading hardcoded fallback: %v", err)
r.loadFallback()
}
r.wg.Add(1)
go func() {
defer r.wg.Done()
ticker := time.NewTicker(modelRefreshInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
if err := r.refresh(ctx); err != nil {
r.logger.Printf("model registry: refresh failed: %v", err)
}
cancel()
case <-r.stopCh:
return
}
}
}()
}
func (r *ModelRegistry) Stop() {
close(r.stopCh)
r.wg.Wait()
}
// Models returns the deduplicated list of all available model names.
func (r *ModelRegistry) Models() []string {
r.mu.RLock()
defer r.mu.RUnlock()
out := make([]string, len(r.allModels))
copy(out, r.allModels)
return out
}
// HasModel checks if the given model is available.
func (r *ModelRegistry) HasModel(model string) bool {
r.mu.RLock()
defer r.mu.RUnlock()
_, ok := r.modelToAgent[model]
return ok
}
// AgentForModel returns the agent ID that should serve the given model.
func (r *ModelRegistry) AgentForModel(model string) (string, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
agent, ok := r.modelToAgent[model]
return agent, ok
}
// AgentIDs returns the list of all known agent IDs.
func (r *ModelRegistry) AgentIDs() []string {
r.mu.RLock()
defer r.mu.RUnlock()
ids := make([]string, 0, len(r.agentModels))
for id := range r.agentModels {
ids = append(ids, id)
}
return ids
}
func (r *ModelRegistry) refresh(ctx context.Context) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, freeAgentsSourceURL, nil)
if err != nil {
return fmt.Errorf("create request: %w", err)
}
req.Header.Set("Accept", "text/plain")
resp, err := r.client.Do(req)
if err != nil {
return fmt.Errorf("fetch free-agents source: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("unexpected status %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read response: %w", err)
}
all := parseAllFreeModels(string(body))
if len(all) == 0 {
return fmt.Errorf("no free agents found in source")
}
modelToAgent, allModels := buildModelMapping(all)
r.mu.Lock()
r.agentModels = all
r.modelToAgent = modelToAgent
r.allModels = allModels
r.lastOK = time.Now()
r.mu.Unlock()
r.logger.Printf("model registry: updated %d agents, %d models: %v", len(all), len(allModels), allModels)
return nil
}
func (r *ModelRegistry) loadFallback() {
modelToAgent, allModels := buildModelMapping(hardcodedFallback)
r.mu.Lock()
r.agentModels = hardcodedFallback
r.modelToAgent = modelToAgent
r.allModels = allModels
r.mu.Unlock()
r.logger.Printf("model registry: loaded fallback models: %v", allModels)
}
// parseAllFreeModels extracts ALL agent→models mappings from the free-agents.ts source.
func parseAllFreeModels(source string) map[string][]string {
blockPattern := regexp.MustCompile(`'([^']+)':\s*new\s+Set\(\[([^\]]*)\]\)`)
modelPattern := regexp.MustCompile(`'([^']+)'`)
result := make(map[string][]string)
for _, match := range blockPattern.FindAllStringSubmatch(source, -1) {
agentID := match[1]
modelsStr := match[2]
var models []string
for _, modelMatch := range modelPattern.FindAllStringSubmatch(modelsStr, -1) {
model := strings.TrimSpace(modelMatch[1])
if model != "" {
models = append(models, model)
}
}
if len(models) > 0 {
result[agentID] = models
}
}
return result
}
// buildModelMapping creates the model→agent reverse mapping and deduplicated model list.
// When a model appears in multiple agents, one is chosen at random.
func buildModelMapping(agentModels map[string][]string) (map[string]string, []string) {
modelAgents := make(map[string][]string)
for agentID, models := range agentModels {
for _, model := range models {
modelAgents[model] = append(modelAgents[model], agentID)
}
}
modelToAgent := make(map[string]string, len(modelAgents))
allModels := make([]string, 0, len(modelAgents))
for model, agents := range modelAgents {
modelToAgent[model] = agents[rand.Intn(len(agents))]
allModels = append(allModels, model)
}
sort.Strings(allModels)
return modelToAgent, allModels
}