/* Dynagate LLM Gateway: dynamically change models on the fly Created by Luxferre in 2026, released into the public domain */ package main import ( "context" "flag" "fmt" "log" "net/http" "os" "os/exec" "os/signal" "strings" "syscall" "time" ) func main() { port := flag.Int("port", 8080, "Port to listen on") csvPath := flag.String("csv", "models.csv", "Path to the CSV configuration file") keyPath := flag.String("key", "", "Path to plaintext file containing expected auth token") csvUpdater := flag.String("csv-updater", "", "Command to run to update the configuration CSV file") csvUpdateInterval := flag.Int("csv-update-interval", 10, "Interval in minutes with which to run the CSV updater command") flag.Parse() var authTokens []string if *keyPath != "" { content, err := os.ReadFile(*keyPath) if err != nil { log.Fatalf("Failed to read auth token file: %v", err) } var tokens []string for _, line := range strings.Split(string(content), "\n") { token := strings.TrimSpace(line) if token != "" { tokens = append(tokens, token) } } if len(tokens) > 0 { authTokens = tokens log.Printf("Starting Dynagate LLM Gateway. Config: %s, Port: %d, Auth: Required (%d tokens loaded)", *csvPath, *port, len(authTokens)) } else { log.Printf("Starting Dynagate LLM Gateway. Config: %s, Port: %d, Auth: None (empty key file)", *csvPath, *port) } } else { log.Printf("Starting Dynagate LLM Gateway. Config: %s, Port: %d, Auth: None", *csvPath, *port) } // Initialize configuration manager cm := NewConfigManager(*csvPath) ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Start background config watcher go cm.Watch(ctx) // Start background CSV updater if specified if *csvUpdater != "" { mins := *csvUpdateInterval if mins <= 0 { log.Printf("Warning: csv-update-interval must be positive, defaulting to 10 minutes") mins = 10 } interval := time.Duration(mins) * time.Minute log.Printf("Starting background CSV updater: command=%q, interval=%v", *csvUpdater, interval) go startCSVUpdater(ctx, *csvUpdater, interval) } // Register routes mux := http.NewServeMux() mux.HandleFunc("/v1/models", handleModels(cm, authTokens)) mux.HandleFunc("/v1/chat/completions", handleChatCompletions(cm, authTokens)) mux.HandleFunc("/v1/images/generations", handleImageGenerations(cm, authTokens)) server := &http.Server{ Addr: fmt.Sprintf(":%d", *port), Handler: mux, } // Graceful shutdown setup shutdownSig := make(chan os.Signal, 1) signal.Notify(shutdownSig, os.Interrupt, syscall.SIGTERM) go func() { log.Printf("Server listening on port %d", *port) if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("Server listen failed: %v", err) } }() <-shutdownSig log.Println("Shutting down Dynagate server...") cancel() // stop config watcher shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) defer shutdownCancel() if err := server.Shutdown(shutdownCtx); err != nil { log.Fatalf("Server shutdown failed: %v", err) } log.Println("Server exited cleanly") } func startCSVUpdater(ctx context.Context, command string, interval time.Duration) { runUpdater := func() { log.Printf("Running CSV updater command: %s", command) cmd := exec.Command("/bin/sh", "-c", command) output, err := cmd.CombinedOutput() if err != nil { log.Printf("CSV updater command failed: %v, output: %q", err, strings.TrimSpace(string(output))) } else { log.Printf("CSV updater command completed successfully") } } // Run once immediately at startup runUpdater() ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: runUpdater() } } }