Enha: atomic global vars instead of mutexes

This commit is contained in:
Grail Finder
2026-03-07 11:26:07 +03:00
parent a842b00e96
commit 8c4d01ab3b
4 changed files with 75 additions and 70 deletions

48
bot.go
View File

@@ -22,7 +22,7 @@ import (
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
"sync" "sync/atomic"
"time" "time"
) )
@@ -49,7 +49,6 @@ var (
//nolint:unused // TTS_ENABLED conditionally uses this //nolint:unused // TTS_ENABLED conditionally uses this
orator Orator orator Orator
asr STT asr STT
localModelsMu sync.RWMutex
defaultLCPProps = map[string]float32{ defaultLCPProps = map[string]float32{
"temperature": 0.8, "temperature": 0.8,
"dry_multiplier": 0.0, "dry_multiplier": 0.0,
@@ -64,11 +63,17 @@ var (
"google/gemma-3-27b-it:free", "google/gemma-3-27b-it:free",
"meta-llama/llama-3.3-70b-instruct:free", "meta-llama/llama-3.3-70b-instruct:free",
} }
LocalModels = []string{} LocalModels atomic.Value // stores []string
localModelsData *models.LCPModels localModelsData atomic.Value // stores *models.LCPModels
orModelsData *models.ORModels orModelsData atomic.Value // stores *models.ORModels
) )
func init() {
LocalModels.Store([]string{})
localModelsData.Store((*models.LCPModels)(nil))
orModelsData.Store((*models.ORModels)(nil))
}
var thinkBlockRE = regexp.MustCompile(`(?s)<think>.*?</think>`) var thinkBlockRE = regexp.MustCompile(`(?s)<think>.*?</think>`)
// parseKnownToTag extracts known_to list from content using configured tag. // parseKnownToTag extracts known_to list from content using configured tag.
@@ -356,7 +361,7 @@ func fetchORModels(free bool) ([]string, error) {
if err := json.NewDecoder(resp.Body).Decode(data); err != nil { if err := json.NewDecoder(resp.Body).Decode(data); err != nil {
return nil, err return nil, err
} }
orModelsData = data orModelsData.Store(data)
freeModels := data.ListModels(free) freeModels := data.ListModels(free)
return freeModels, nil return freeModels, nil
} }
@@ -418,9 +423,7 @@ func fetchLCPModelsWithStatus() (*models.LCPModels, error) {
if err := json.NewDecoder(resp.Body).Decode(data); err != nil { if err := json.NewDecoder(resp.Body).Decode(data); err != nil {
return nil, err return nil, err
} }
localModelsMu.Lock() localModelsData.Store(data)
localModelsData = data
localModelsMu.Unlock()
return data, nil return data, nil
} }
@@ -1403,7 +1406,7 @@ func charToStart(agentName string, keepSysP bool) bool {
func updateModelLists() { func updateModelLists() {
var err error var err error
if cfg.OpenRouterToken != "" { if cfg.OpenRouterToken != "" {
ORFreeModels, err = fetchORModels(true) _, err := fetchORModels(true)
if err != nil { if err != nil {
logger.Warn("failed to fetch or models", "error", err) logger.Warn("failed to fetch or models", "error", err)
} }
@@ -1413,22 +1416,19 @@ func updateModelLists() {
if err != nil { if err != nil {
logger.Warn("failed to fetch llama.cpp models", "error", err) logger.Warn("failed to fetch llama.cpp models", "error", err)
} }
localModelsMu.Lock() LocalModels.Store(ml)
LocalModels = ml
localModelsMu.Unlock()
for statusLineWidget == nil { for statusLineWidget == nil {
time.Sleep(time.Millisecond * 100) time.Sleep(time.Millisecond * 100)
} }
// set already loaded model in llama.cpp // set already loaded model in llama.cpp
if strings.Contains(cfg.CurrentAPI, "localhost") || strings.Contains(cfg.CurrentAPI, "127.0.0.1") { if strings.Contains(cfg.CurrentAPI, "localhost") || strings.Contains(cfg.CurrentAPI, "127.0.0.1") {
localModelsMu.Lock() modelList := LocalModels.Load().([]string)
defer localModelsMu.Unlock() for i := range modelList {
for i := range LocalModels { if strings.Contains(modelList[i], models.LoadedMark) {
if strings.Contains(LocalModels[i], models.LoadedMark) { m := strings.TrimPrefix(modelList[i], models.LoadedMark)
m := strings.TrimPrefix(LocalModels[i], models.LoadedMark)
cfg.CurrentModel = m cfg.CurrentModel = m
chatBody.Model = m chatBody.Model = m
cachedModelColor = "green" cachedModelColor.Store("green")
updateStatusLine() updateStatusLine()
updateToolCapabilities() updateToolCapabilities()
app.Draw() app.Draw()
@@ -1439,21 +1439,17 @@ func updateModelLists() {
} }
func refreshLocalModelsIfEmpty() { func refreshLocalModelsIfEmpty() {
localModelsMu.RLock() models := LocalModels.Load().([]string)
if len(LocalModels) > 0 { if len(models) > 0 {
localModelsMu.RUnlock()
return return
} }
localModelsMu.RUnlock()
// try to fetch // try to fetch
models, err := fetchLCPModels() models, err := fetchLCPModels()
if err != nil { if err != nil {
logger.Warn("failed to fetch llama.cpp models", "error", err) logger.Warn("failed to fetch llama.cpp models", "error", err)
return return
} }
localModelsMu.Lock() LocalModels.Store(models)
LocalModels = models
localModelsMu.Unlock()
} }
func summarizeAndStartNewChat() { func summarizeAndStartNewChat() {

View File

@@ -16,11 +16,17 @@ import (
"time" "time"
"unicode" "unicode"
"sync/atomic"
"github.com/rivo/tview" "github.com/rivo/tview"
) )
// Cached model color - updated by background goroutine // Cached model color - updated by background goroutine
var cachedModelColor string = "orange" var cachedModelColor atomic.Value // stores string
func init() {
cachedModelColor.Store("orange")
}
// startModelColorUpdater starts a background goroutine that periodically updates // startModelColorUpdater starts a background goroutine that periodically updates
// the cached model color. Only runs HTTP requests for local llama.cpp APIs. // the cached model color. Only runs HTTP requests for local llama.cpp APIs.
@@ -39,20 +45,20 @@ func startModelColorUpdater() {
// updateCachedModelColor updates the global cachedModelColor variable // updateCachedModelColor updates the global cachedModelColor variable
func updateCachedModelColor() { func updateCachedModelColor() {
if !isLocalLlamacpp() { if !isLocalLlamacpp() {
cachedModelColor = "orange" cachedModelColor.Store("orange")
return return
} }
// Check if model is loaded // Check if model is loaded
loaded, err := isModelLoaded(chatBody.GetModel()) loaded, err := isModelLoaded(chatBody.GetModel())
if err != nil { if err != nil {
// On error, assume not loaded (red) // On error, assume not loaded (red)
cachedModelColor = "red" cachedModelColor.Store("red")
return return
} }
if loaded { if loaded {
cachedModelColor = "green" cachedModelColor.Store("green")
} else { } else {
cachedModelColor = "red" cachedModelColor.Store("red")
} }
} }
@@ -335,7 +341,7 @@ func isLocalLlamacpp() bool {
// The cached value is updated by a background goroutine every 5 seconds. // The cached value is updated by a background goroutine every 5 seconds.
// For non-local models, returns orange. For local llama.cpp models, returns green if loaded, red if not. // For non-local models, returns orange. For local llama.cpp models, returns green if loaded, red if not.
func getModelColor() string { func getModelColor() string {
return cachedModelColor return cachedModelColor.Load().(string)
} }
func makeStatusLine() string { func makeStatusLine() string {
@@ -421,40 +427,48 @@ func getMaxContextTokens() int {
modelName := chatBody.GetModel() modelName := chatBody.GetModel()
switch { switch {
case strings.Contains(cfg.CurrentAPI, "openrouter"): case strings.Contains(cfg.CurrentAPI, "openrouter"):
if orModelsData != nil { ord := orModelsData.Load()
for i := range orModelsData.Data { if ord != nil {
m := &orModelsData.Data[i] data := ord.(*models.ORModels)
if m.ID == modelName { if data != nil {
return m.ContextLength for i := range data.Data {
m := &data.Data[i]
if m.ID == modelName {
return m.ContextLength
}
} }
} }
} }
case strings.Contains(cfg.CurrentAPI, "deepseek"): case strings.Contains(cfg.CurrentAPI, "deepseek"):
return deepseekContext return deepseekContext
default: default:
if localModelsData != nil { lmd := localModelsData.Load()
for i := range localModelsData.Data { if lmd != nil {
m := &localModelsData.Data[i] data := lmd.(*models.LCPModels)
if m.ID == modelName { if data != nil {
for _, arg := range m.Status.Args { for i := range data.Data {
if strings.HasPrefix(arg, "--ctx-size") { m := &data.Data[i]
if strings.Contains(arg, "=") { if m.ID == modelName {
val := strings.Split(arg, "=")[1] for _, arg := range m.Status.Args {
if n, err := strconv.Atoi(val); err == nil { if strings.HasPrefix(arg, "--ctx-size") {
return n if strings.Contains(arg, "=") {
} val := strings.Split(arg, "=")[1]
} else { if n, err := strconv.Atoi(val); err == nil {
idx := -1
for j, a := range m.Status.Args {
if a == "--ctx-size" && j+1 < len(m.Status.Args) {
idx = j + 1
break
}
}
if idx != -1 {
if n, err := strconv.Atoi(m.Status.Args[idx]); err == nil {
return n return n
} }
} else {
idx := -1
for j, a := range m.Status.Args {
if a == "--ctx-size" && j+1 < len(m.Status.Args) {
idx = j + 1
break
}
}
if idx != -1 {
if n, err := strconv.Atoi(m.Status.Args[idx]); err == nil {
return n
}
}
} }
} }
} }

View File

@@ -22,7 +22,7 @@ func showModelSelectionPopup() {
models, err := fetchLCPModelsWithLoadStatus() models, err := fetchLCPModelsWithLoadStatus()
if err != nil { if err != nil {
logger.Error("failed to fetch models with load status", "error", err) logger.Error("failed to fetch models with load status", "error", err)
return LocalModels return LocalModels.Load().([]string)
} }
return models return models
} }
@@ -30,7 +30,8 @@ func showModelSelectionPopup() {
modelList := getModelListForAPI(cfg.CurrentAPI) modelList := getModelListForAPI(cfg.CurrentAPI)
// Check for empty options list // Check for empty options list
if len(modelList) == 0 { if len(modelList) == 0 {
logger.Warn("empty model list for", "api", cfg.CurrentAPI, "localModelsLen", len(LocalModels), "orModelsLen", len(ORFreeModels)) localModels := LocalModels.Load().([]string)
logger.Warn("empty model list for", "api", cfg.CurrentAPI, "localModelsLen", len(localModels), "orModelsLen", len(ORFreeModels))
var message string var message string
switch { switch {
case strings.Contains(cfg.CurrentAPI, "openrouter.ai"): case strings.Contains(cfg.CurrentAPI, "openrouter.ai"):
@@ -150,9 +151,7 @@ func showAPILinkSelectionPopup() {
} }
// Assume local llama.cpp // Assume local llama.cpp
refreshLocalModelsIfEmpty() refreshLocalModelsIfEmpty()
localModelsMu.RLock() return LocalModels.Load().([]string)
defer localModelsMu.RUnlock()
return LocalModels
} }
newModelList := getModelListForAPI(cfg.CurrentAPI) newModelList := getModelListForAPI(cfg.CurrentAPI)
// Ensure chatBody.Model is in the new list; if not, set to first available model // Ensure chatBody.Model is in the new list; if not, set to first available model

View File

@@ -4,14 +4,11 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"sync"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/rivo/tview" "github.com/rivo/tview"
) )
var _ = sync.RWMutex{}
// Define constants for cell types // Define constants for cell types
const ( const (
CellTypeCheckbox = "checkbox" CellTypeCheckbox = "checkbox"
@@ -157,9 +154,7 @@ func makePropsTable(props map[string]float32) *tview.Table {
} }
// Assume local llama.cpp // Assume local llama.cpp
refreshLocalModelsIfEmpty() refreshLocalModelsIfEmpty()
localModelsMu.RLock() return LocalModels.Load().([]string)
defer localModelsMu.RUnlock()
return LocalModels
} }
// Add input fields // Add input fields
addInputRow("New char to write msg as", "", func(text string) { addInputRow("New char to write msg as", "", func(text string) {
@@ -262,7 +257,8 @@ func makePropsTable(props map[string]float32) *tview.Table {
// Check for empty options list // Check for empty options list
if len(data.Options) == 0 { if len(data.Options) == 0 {
logger.Warn("empty options list for", "label", label, "api", cfg.CurrentAPI, "localModelsLen", len(LocalModels), "orModelsLen", len(ORFreeModels)) localModels := LocalModels.Load().([]string)
logger.Warn("empty options list for", "label", label, "api", cfg.CurrentAPI, "localModelsLen", len(localModels), "orModelsLen", len(ORFreeModels))
message := "No options available for " + label message := "No options available for " + label
if label == "Select a model" { if label == "Select a model" {
switch { switch {