diff --git a/.gitignore b/.gitignore index a3824050..e7c93c57 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ server.pid /target nohup.out *.bak +.antigravitycli/ + diff --git a/README.md b/README.md index 4db8b052..ac31cad1 100644 --- a/README.md +++ b/README.md @@ -239,7 +239,7 @@ Pre-seeded groups: | `dispatcher` | - | classifier | fast-flow, standard-pro, heavy-logic | Auto-dispatches by complexity | | `deepseek-auto` | - | heuristic | deepseek-chat, deepseek-reasoner | Legacy provider group | | `openai-auto` | - | heuristic | gpt-4o-mini, gpt-4o | Legacy provider group | -| `gemini-auto` | - | heuristic | gemini-2.0-flash, gemini-2.5-pro | Legacy provider group | +| `gemini-auto` | - | heuristic | gemini-2.5-flash, gemini-2.5-pro | Legacy provider group | ### Image Generation (DALL-E / Imagen) diff --git a/deploy.sh b/deploy.sh old mode 100644 new mode 100755 index c5da7ee7..55c111ea --- a/deploy.sh +++ b/deploy.sh @@ -1,23 +1,25 @@ #!/bin/bash -# Define the service name/path for easy updates +set -e + BINARY_NAME="gophergate" SOURCE_PATH="./cmd/gophergate/main.go" -echo "Stopping existing $BINARY_NAME processes..." -# Using pkill; || true ensures the script continues even if no process was found -pkill -9 "$BINARY_NAME" || echo "No running process found." - echo "Pulling latest changes from git..." +git stash || true git pull echo "Building the application..." -if go build -o "$BINARY_NAME" "$SOURCE_PATH"; then - echo "Build successful. Starting $BINARY_NAME in the background..." - # Launch with nohup and redirect output to a log file - nohup "./$BINARY_NAME" > gophergate.log 2>&1 & - echo "Service started. PID: $!" +go build -o "$BINARY_NAME" "$SOURCE_PATH" + +echo "Restarting service..." +systemctl restart gophergate + +sleep 2 +if systemctl is-active --quiet gophergate; then + echo "Deploy complete. Service is running." + systemctl status gophergate --no-pager | head -5 else - echo "Build failed! Keeping the previous state." + echo "Service failed to start! Check: journalctl -u gophergate -n 20" exit 1 fi diff --git a/internal/config/config.go b/internal/config/config.go index 53e5670b..f1f4bcd2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -106,7 +106,7 @@ func Load() (*Config, error) { v.SetDefault("providers.gemini.api_key_env", "GEMINI_API_KEY") v.SetDefault("providers.gemini.base_url", "https://generativelanguage.googleapis.com/v1") - v.SetDefault("providers.gemini.default_model", "gemini-2.0-flash") + v.SetDefault("providers.gemini.default_model", "gemini-2.5-flash") v.SetDefault("providers.gemini.enabled", true) v.SetDefault("providers.deepseek.api_key_env", "DEEPSEEK_API_KEY") @@ -116,7 +116,7 @@ func Load() (*Config, error) { v.SetDefault("providers.moonshot.api_key_env", "MOONSHOT_API_KEY") v.SetDefault("providers.moonshot.base_url", "https://api.moonshot.ai/v1") - v.SetDefault("providers.moonshot.default_model", "kimi-k2.5") + v.SetDefault("providers.moonshot.default_model", "kimi-k2.7-code") v.SetDefault("providers.moonshot.enabled", true) v.SetDefault("providers.grok.api_key_env", "GROK_API_KEY") diff --git a/internal/db/db.go b/internal/db/db.go index 18d40668..aee1929f 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -209,7 +209,7 @@ func (db *DB) RunMigrations() error { }{ {"deepseek-auto", "heuristic", `["deepseek-chat","deepseek-reasoner"]`, "", nil, nil, nil}, {"openai-auto", "heuristic", `["gpt-4o-mini","gpt-4o"]`, "", nil, nil, nil}, - {"gemini-auto", "heuristic", `["gemini-2.0-flash","gemini-2.5-pro"]`, "", nil, nil, nil}, + {"gemini-auto", "heuristic", `["gemini-2.5-flash","gemini-2.5-pro"]`, "", nil, nil, nil}, {"heavy-logic", "heuristic", `["grok-4.3","kimi-k2.6","deepseek-v4-pro"]`, "", nil, intPtr(9), strPtr("Complex Coding, Logic, Agents.")}, {"standard-pro", "heuristic", `["gpt-5.4-mini","gemini-3-flash-preview"]`, "", nil, intPtr(5), strPtr("General Assistant, Long Docs.")}, {"fast-flow", "heuristic", `["deepseek-v4-flash","gpt-5.4-nano"]`, "", nil, intPtr(2), strPtr("Classification, JSON, Basic Q&A.")}, diff --git a/internal/providers/gemini.go b/internal/providers/gemini.go index b72f6df2..27454bc7 100644 --- a/internal/providers/gemini.go +++ b/internal/providers/gemini.go @@ -201,6 +201,16 @@ func (p *GeminiProvider) ResponsesStream(ctx context.Context, req *models.Respon } func (p *GeminiProvider) ChatCompletion(ctx context.Context, req *models.UnifiedRequest) (*models.ChatCompletionResponse, error) { + // Map deprecated or preview model names to active equivalents + switch req.Model { + case "gemini-2.0-flash": + req.Model = "gemini-2.5-flash" + case "gemini-3-flash": + req.Model = "gemini-3-flash-preview" + case "gemini-3-pro": + req.Model = "gemini-3-pro-preview" + } + // Gemini mapping var contents []GeminiContent @@ -354,7 +364,10 @@ func (p *GeminiProvider) ChatCompletion(ctx context.Context, req *models.Unified baseURL := p.config.BaseURL lowerModel := strings.ToLower(req.Model) - if strings.Contains(lowerModel, "preview") || strings.Contains(lowerModel, "3.1") || strings.Contains(lowerModel, "2.0") || strings.Contains(lowerModel, "thinking") || hasMappedTools { + if strings.Contains(lowerModel, "preview") || + strings.Contains(lowerModel, "thinking") || + strings.Contains(lowerModel, "gemini-") || + hasMappedTools { // Use v1beta for preview, newer models, or when using tools if !strings.Contains(baseURL, "v1beta") { baseURL = strings.Replace(baseURL, "/v1", "/v1beta", 1) @@ -468,6 +481,16 @@ func (p *GeminiProvider) ChatCompletion(ctx context.Context, req *models.Unified } func (p *GeminiProvider) ChatCompletionStream(ctx context.Context, req *models.UnifiedRequest) (<-chan *models.ChatCompletionStreamResponse, error) { + // Map deprecated or preview model names to active equivalents + switch req.Model { + case "gemini-2.0-flash": + req.Model = "gemini-2.5-flash" + case "gemini-3-flash": + req.Model = "gemini-3-flash-preview" + case "gemini-3-pro": + req.Model = "gemini-3-pro-preview" + } + // Simplified Gemini mapping var contents []GeminiContent for i := 0; i < len(req.Messages); i++ { @@ -598,7 +621,10 @@ func (p *GeminiProvider) ChatCompletionStream(ctx context.Context, req *models.U baseURL := p.config.BaseURL lowerModel := strings.ToLower(req.Model) - if strings.Contains(lowerModel, "preview") || strings.Contains(lowerModel, "3.1") || strings.Contains(lowerModel, "2.0") || strings.Contains(lowerModel, "thinking") || hasMappedTools { + if strings.Contains(lowerModel, "preview") || + strings.Contains(lowerModel, "thinking") || + strings.Contains(lowerModel, "gemini-") || + hasMappedTools { // Use v1beta for preview, newer models, or when using tools if !strings.Contains(baseURL, "v1beta") { baseURL = strings.Replace(baseURL, "/v1", "/v1beta", 1) diff --git a/internal/providers/moonshot.go b/internal/providers/moonshot.go index 66e90953..401d19c2 100644 --- a/internal/providers/moonshot.go +++ b/internal/providers/moonshot.go @@ -44,6 +44,11 @@ func (p *MoonshotProvider) ChatCompletion(ctx context.Context, req *models.Unifi body["max_completion_tokens"] = maxTokens } } + if strings.Contains(strings.ToLower(req.Model), "kimi-k2.6") { + if _, ok := body["temperature"]; ok { + body["temperature"] = 1.0 + } + } baseURL := strings.TrimRight(p.config.BaseURL, "/") @@ -90,6 +95,11 @@ func (p *MoonshotProvider) ChatCompletionStream(ctx context.Context, req *models body["max_completion_tokens"] = maxTokens } } + if strings.Contains(strings.ToLower(req.Model), "kimi-k2.6") { + if _, ok := body["temperature"]; ok { + body["temperature"] = 1.0 + } + } baseURL := strings.TrimRight(p.config.BaseURL, "/") diff --git a/internal/router/heuristic.go b/internal/router/heuristic.go index e10586b9..6df7c5b4 100644 --- a/internal/router/heuristic.go +++ b/internal/router/heuristic.go @@ -26,12 +26,13 @@ type ConditionRule struct { // Conditions defines the matching parameters for a rule. type Conditions struct { - AnyOfTags []string `json:"any_of_tags,omitempty"` - MaxInputTokensLt *int `json:"max_input_tokens_lt,omitempty"` - RequiresReasoning *bool `json:"requires_reasoning,omitempty"` - RequiresToolCalling *bool `json:"requires_tool_calling,omitempty"` - HasMultimodalInput *bool `json:"has_multimodal_input,omitempty"` - IsDefaultFallback *bool `json:"is_default_fallback,omitempty"` + AnyOfTags []string `json:"any_of_tags,omitempty"` + MaxInputTokensLt *int `json:"max_input_tokens_lt,omitempty"` + RequiresReasoning *bool `json:"requires_reasoning,omitempty"` + RequiresToolCalling *bool `json:"requires_tool_calling,omitempty"` + IsSoftwareDevelopment *bool `json:"is_software_development,omitempty"` + HasMultimodalInput *bool `json:"has_multimodal_input,omitempty"` + IsDefaultFallback *bool `json:"is_default_fallback,omitempty"` } func routeHeuristic(group db.ModelGroup, targets []string, routeCtx *RouteContext) (*Decision, error) { @@ -198,6 +199,13 @@ func matchConditions(cond Conditions, routeCtx *RouteContext) bool { } } + // Check software development flag + if cond.IsSoftwareDevelopment != nil { + if routeCtx.IsSoftwareDevelopment != *cond.IsSoftwareDevelopment { + return false + } + } + // Check multimodal flag if cond.HasMultimodalInput != nil { if routeCtx.HasMultimodalInput != *cond.HasMultimodalInput { diff --git a/internal/router/heuristic_test.go b/internal/router/heuristic_test.go index 4029e9cd..e7895072 100644 --- a/internal/router/heuristic_test.go +++ b/internal/router/heuristic_test.go @@ -140,3 +140,65 @@ func TestRouteHeuristic_LegacyRules(t *testing.T) { t.Fatalf("expected kimi-k2.6, got %s", dec2.SelectedModel) } } + +func TestRouteHeuristic_SoftwareDevelopmentCondition(t *testing.T) { + targets := []string{ + "gemini-3-flash", + "kimi-k2.7-code", + "deepseek-v4-pro", + "mimo-v2.5-pro", + } + + rulesJSON := `[ + { + "rule_id": "agentic_code_and_tools", + "conditions": { + "requires_tool_calling": true, + "is_software_development": true + }, + "primary_model": "kimi-k2.7-code", + "fallback_model": "deepseek-v4-pro" + }, + { + "rule_id": "agentic_general_tasks", + "conditions": { + "requires_tool_calling": true + }, + "primary_model": "mimo-v2.5-pro" + } + ]` + + group := db.ModelGroup{ + ID: "dustins_stack", + Strategy: "heuristic", + HeuristicRules: &rulesJSON, + } + + // 1. Tool calling + Software dev should route to kimi-k2.7-code + ctx1 := &RouteContext{ + UserMessage: "Write a python script to parse logs", + RequiresToolCalling: true, + IsSoftwareDevelopment: true, + } + dec1, err := routeHeuristic(group, targets, ctx1) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if dec1.SelectedModel != "kimi-k2.7-code" { + t.Fatalf("expected kimi-k2.7-code, got %s", dec1.SelectedModel) + } + + // 2. Tool calling but NOT Software dev should route to mimo-v2.5-pro + ctx2 := &RouteContext{ + UserMessage: "Search the web for weather in New York", + RequiresToolCalling: true, + IsSoftwareDevelopment: false, + } + dec2, err := routeHeuristic(group, targets, ctx2) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if dec2.SelectedModel != "mimo-v2.5-pro" { + t.Fatalf("expected mimo-v2.5-pro, got %s (reason: %s)", dec2.SelectedModel, dec2.Reason) + } +} diff --git a/internal/router/router.go b/internal/router/router.go index 8007392e..9689acb5 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -18,12 +18,13 @@ type Decision struct { // RouteContext holds metadata of the request to evaluate condition rules. type RouteContext struct { - UserMessage string `json:"user_message"` - InputTokens int `json:"input_tokens"` - HasMultimodalInput bool `json:"has_multimodal_input"` - RequiresToolCalling bool `json:"requires_tool_calling"` - RequiresReasoning bool `json:"requires_reasoning"` - Tags []string `json:"tags"` + UserMessage string `json:"user_message"` + InputTokens int `json:"input_tokens"` + HasMultimodalInput bool `json:"has_multimodal_input"` + RequiresToolCalling bool `json:"requires_tool_calling"` + RequiresReasoning bool `json:"requires_reasoning"` + IsSoftwareDevelopment bool `json:"is_software_development"` + Tags []string `json:"tags"` } // ClassifierFunc is the callback for classifier-based routing. diff --git a/internal/server/models_config.go b/internal/server/models_config.go index 5deee261..f3d07418 100644 --- a/internal/server/models_config.go +++ b/internal/server/models_config.go @@ -49,6 +49,7 @@ func (s *Server) handleGetModels(c *gin.Context) { var result []gin.H s.registryMu.RLock() + defer s.registryMu.RUnlock() if s.registry != nil { for pID, pInfo := range s.registry.Providers { proxyProvider, allowed := allowedRegistryProviders[pID] @@ -208,6 +209,7 @@ func (s *Server) handleUpdateModel(c *gin.Context) { } } } + s.registryMu.RUnlock() _, err := s.database.Exec(` INSERT INTO model_configs (id, provider_id, enabled, prompt_cost_per_m, completion_cost_per_m, cache_read_cost_per_m, cache_write_cost_per_m, mapping) diff --git a/internal/server/providers_admin.go b/internal/server/providers_admin.go index b8e020c9..d785cb72 100644 --- a/internal/server/providers_admin.go +++ b/internal/server/providers_admin.go @@ -226,11 +226,11 @@ func (s *Server) handleTestProvider(c *gin.Context) { // Adjust model for non-openai providers if name == "gemini" { - testReq.Model = "gemini-2.0-flash" + testReq.Model = "gemini-2.5-flash" } else if name == "deepseek" { testReq.Model = "deepseek-chat" } else if name == "moonshot" { - testReq.Model = "kimi-k2.5" + testReq.Model = "kimi-k2.7-code" } else if name == "grok" { testReq.Model = "grok-4-1-fast-non-reasoning" } else if name == "xiaomi" { diff --git a/internal/server/routing_test.go b/internal/server/routing_test.go new file mode 100644 index 00000000..4b91bdd4 --- /dev/null +++ b/internal/server/routing_test.go @@ -0,0 +1,31 @@ +package server + +import "testing" + +func TestIsSoftwareDevelopment(t *testing.T) { + tests := []struct { + message string + expected bool + }{ + {"can you check the logs? it looks like a lot of requests are being routed to kimi-k2.7-code when they don't need to be", false}, + {"it looks like its still routing to kimi when it shouldn't need to", false}, + {"Write a python script to parse logs", true}, + {"Search the web for weather in New York", false}, + {"How to build a compiler in Go", true}, + {"Check my vscode config", false}, + {"Let's decode this barcode", false}, + {"go ahead and email both docxs and the ppt to kayla\n\n... HELPipedia_Telethon_Speaker_Script.docx ... mental health program specialist ...", false}, + {"academic program development", false}, + {"write a script for the video presentation", false}, + {"How to write a bash script", true}, + {"refactor this sql query", true}, + {"can you organize Downloads and Documents real quick?\n\n\nCLI wrapper: ~/Projects/rag-engine/rag (bash script activating .venv). Python venv@~/Projects/rag-engine/.venv/.\n", false}, + } + + for _, tt := range tests { + result := isSoftwareDevelopment(tt.message) + if result != tt.expected { + t.Errorf("isSoftwareDevelopment(%q) = %v; expected %v", tt.message, result, tt.expected) + } + } +} diff --git a/internal/server/server.go b/internal/server/server.go index 668e9209..dc4db7d9 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,12 +1,13 @@ package server import ( - "encoding/json" "context" + "encoding/json" "fmt" "io" "log" "net/http" + "regexp" "strings" "sync" "time" @@ -57,7 +58,9 @@ func NewServer(cfg *config.Config, database *db.DB) *Server { if err != nil { fmt.Printf("Warning: Failed to fetch initial model registry: %v\n", err) } else { + s.registryMu.Lock() s.registry = registry + s.registryMu.Unlock() } }() @@ -976,12 +979,14 @@ func (s *Server) buildRouteContextFromChat(req models.ChatCompletionRequest) *ro strings.Contains(msgLower, "explain in detail") routeCtx := &router.RouteContext{ - UserMessage: userMessage, - InputTokens: inputTokens, - HasMultimodalInput: hasMultimodal, - RequiresToolCalling: requiresToolCalling, - RequiresReasoning: requiresReasoning, + UserMessage: userMessage, + InputTokens: inputTokens, + HasMultimodalInput: hasMultimodal, + RequiresToolCalling: requiresToolCalling, + RequiresReasoning: requiresReasoning, + IsSoftwareDevelopment: isSoftwareDevelopment(userMessage), } + log.Printf("[DEBUG] RouteContext built: msg=%q, IsSoftwareDevelopment=%v", userMessage, routeCtx.IsSoftwareDevelopment) routeCtx.Tags = s.getRouteCtxTags(routeCtx) return routeCtx } @@ -1034,12 +1039,14 @@ func (s *Server) buildRouteContextFromResponses(req models.ResponsesRequest) *ro strings.Contains(msgLower, "explain in detail") routeCtx := &router.RouteContext{ - UserMessage: userMessage, - InputTokens: inputTokens, - HasMultimodalInput: hasMultimodal, - RequiresToolCalling: requiresToolCalling, - RequiresReasoning: requiresReasoning, + UserMessage: userMessage, + InputTokens: inputTokens, + HasMultimodalInput: hasMultimodal, + RequiresToolCalling: requiresToolCalling, + RequiresReasoning: requiresReasoning, + IsSoftwareDevelopment: isSoftwareDevelopment(userMessage), } + log.Printf("[DEBUG] RouteContext built: msg=%q, IsSoftwareDevelopment=%v", userMessage, routeCtx.IsSoftwareDevelopment) routeCtx.Tags = s.getRouteCtxTags(routeCtx) return routeCtx } @@ -1102,3 +1109,35 @@ func (s *Server) getRouteCtxTags(routeCtx *router.RouteContext) []string { return tags } + +var ( + modelStripRegex = regexp.MustCompile(`(?i)\b[a-z0-9/._:-]*(code|coder|codex)[a-z0-9/._:-]*\b`) + memoryContextRegex = regexp.MustCompile(`(?s).*?`) + softwareDevRegex = regexp.MustCompile(`(?i)\b(code|coding|programmer|programming|software|refactor|refactoring|compile|compiling|compiler|git|github|debug|debugging|repository|repo|unittest|unit test|test suite|test case|test cases|pull request|merge conflict|syntax error|source code|python|golang|javascript|typescript|ruby|php|perl|swift|kotlin|scala|rustlang|bash|powershell|sql|script|snippet|api|endpoint|patch|diff)\b`) +) + +func isSoftwareDevelopment(userMessage string) bool { + msgLower := strings.ToLower(userMessage) + sanitized := memoryContextRegex.ReplaceAllString(msgLower, "") + sanitized = modelStripRegex.ReplaceAllString(sanitized, "") + + if strings.Contains(sanitized, "script") { + nonCodingScriptTerms := []string{ + "speaker script", "dialogue script", "movie script", "theatre script", + "theater script", "script writing", "script writer", "video script", + "audio script", "podcast script", "script for Kayla", "script for the", + } + hasNonCodingScript := false + for _, term := range nonCodingScriptTerms { + if strings.Contains(sanitized, strings.ToLower(term)) { + hasNonCodingScript = true + break + } + } + if hasNonCodingScript { + sanitized = strings.ReplaceAll(sanitized, "script", "") + } + } + + return softwareDevRegex.MatchString(sanitized) +} diff --git a/internal/server/system.go b/internal/server/system.go index 7a40e77c..2d19ab69 100644 --- a/internal/server/system.go +++ b/internal/server/system.go @@ -67,6 +67,7 @@ func (s *Server) handleGetSettings(c *gin.Context) { providerCount := 0 modelCount := 0 s.registryMu.RLock() + defer s.registryMu.RUnlock() if s.registry != nil { providerCount = len(s.registry.Providers) for _, p := range s.registry.Providers { diff --git a/static/js/pages/monitoring.js b/static/js/pages/monitoring.js index 83c9edc6..11d016ea 100644 --- a/static/js/pages/monitoring.js +++ b/static/js/pages/monitoring.js @@ -492,7 +492,7 @@ class MonitoringPage { simulateRequest() { const clients = ['client-1', 'client-2', 'client-3', 'client-4', 'client-5']; const providers = ['OpenAI', 'Gemini', 'DeepSeek', 'Grok']; - const models = ['gpt-4o', 'gpt-4o-mini', 'gemini-2.0-flash', 'deepseek-chat', 'grok-4-1-fast-non-reasoning']; + const models = ['gpt-4o', 'gpt-4o-mini', 'gemini-2.5-flash', 'deepseek-chat', 'grok-4-1-fast-non-reasoning']; const statuses = ['success', 'success', 'success', 'error', 'warning']; // Mostly success const request = {