From 631a8ebcf7b188690a094bc8912d06c657c1bee7 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 11 Nov 2026 12:17:19 +0000 Subject: [PATCH] Fix SSRF vulnerability in cli_executor.go by properly constructing url.URL Co-authored-by: rschumann <360788+rschumann@users.noreply.github.com> --- .jules/sentinel.md | 4 + cmd/server/main.go | 6 +- internal/access/config_access/provider.go | 8 +- .../access/config_access/provider_test.go | 4 +- internal/api/server.go | 52 ++--- internal/api/server_coordinator_test.go | 4 +- internal/api/statebox_handler_test.go | 58 ++--- internal/autoroute/backoff_test.go | 4 +- internal/autoroute/config.go | 30 +-- internal/autoroute/discovery.go | 10 +- internal/autoroute/e2e_failover_test.go | 6 +- internal/autoroute/experiment.go | 58 ++--- internal/autoroute/experiment_test.go | 14 +- internal/autoroute/health.go | 16 +- internal/autoroute/journal.go | 58 ++--- internal/autoroute/probes/api_probe.go | 4 +- internal/autoroute/probes/ollama_probe.go | 2 +- internal/autoroute/resolver.go | 22 +- internal/autoroute/rqs.go | 24 +- internal/autoroute/rqs_test.go | 6 +- internal/autoroute/scorer_test.go | 24 +- internal/autoroute/types.go | 1 - internal/config/config_integration_test.go | 60 ++--- internal/config/heartbeat.go | 26 +-- internal/config/performance_config_test.go | 14 +- internal/hooks/eventbus.go | 6 +- internal/hooks/eventbus_test.go | 2 +- .../integration/coordinator_heartbeat_test.go | 2 +- internal/integration/coordinator_test.go | 4 +- internal/integration/events_test.go | 2 +- internal/integration/handler_adapter.go | 34 +-- internal/integration/handler_adapter_test.go | 2 +- internal/integration/pipeline_test.go | 8 +- internal/intelligence/cache/semantic_cache.go | 16 +- internal/intelligence/capability/analyzer.go | 62 +++--- .../intelligence/capability/analyzer_test.go | 136 ++++++------ .../intelligence/cascade/quality_signals.go | 16 +- .../discovery/capability_integration_test.go | 26 +-- .../discovery/integration_test.go | 42 ++-- .../intelligence/discovery/service_test.go | 82 +++---- .../intelligence/embedding/locator_test.go | 2 +- .../intelligence/embedding/tokenizer_test.go | 6 +- .../intelligence/feedback/collector_test.go | 62 +++--- .../intelligence/learning_integration_test.go | 28 +-- internal/intelligence/matrix/builder.go | 4 +- internal/intelligence/matrix/builder_test.go | 49 ++--- internal/intelligence/router.go | 32 +-- internal/intelligence/semantic/tier.go | 21 +- internal/intelligence/skills/registry_test.go | 18 +- internal/learning/analysis_test.go | 42 ++-- internal/learning/confidence_test.go | 76 +++---- internal/memory/analytics.go | 206 +++++++++--------- internal/memory/analytics_test.go | 160 +++++++------- internal/memory/benchmark_test.go | 2 +- internal/memory/preferences_test.go | 10 +- internal/memory/property_test.go | 6 +- internal/memory/routing_history.go | 8 +- internal/memory/structure.go | 2 +- internal/observability/events.go | 4 +- internal/observability/metrics.go | 4 +- internal/performance/circuit/breaker.go | 2 +- internal/performance/circuit/breaker_test.go | 6 +- internal/performance/loadshed/middleware.go | 4 +- internal/performance/ratelimit/limiter.go | 6 +- internal/plugin/lua_engine.go | 54 +++-- internal/plugin/mcp_client.go | 6 +- internal/runtime/executor/cli_executor.go | 13 +- internal/steering/injection_test.go | 2 +- internal/superbrain/doctor/doctor_test.go | 8 +- internal/superbrain/executor.go | 11 +- internal/superbrain/executor_test.go | 8 +- internal/superbrain/injector/audit_test.go | 10 +- internal/superbrain/metadata/metadata.go | 18 +- internal/superbrain/metadata/metadata_test.go | 10 +- internal/superbrain/overwatch/ring_buffer.go | 12 +- internal/superbrain/recovery/restart.go | 26 +-- internal/superbrain/recovery/restart_test.go | 16 +- internal/superbrain/router/adapter_test.go | 1 - internal/superbrain/sculptor/analyzer.go | 1 - internal/superbrain/sculptor/analyzer_test.go | 6 +- internal/superbrain/security/failsafe_test.go | 8 +- internal/superbrain/types/types.go | 1 - .../gemini_openai_responses_streaming_test.go | 18 +- internal/util/statebox.go | 8 +- internal/util/think_tag_normalizer.go | 2 +- internal/watcher/watcher.go | 44 ++-- .../handlers/handlers_circuit_breaker_test.go | 20 +- .../notifications/notifications_test.go | 4 +- sdk/switchailocal/auth/conductor.go | 8 +- .../auth/conductor_failover_test.go | 2 +- 90 files changed, 1019 insertions(+), 1013 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 33f2d09a..bd8da261 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -361,3 +361,7 @@ Remember: You're Sentinel, the guardian of switchAILocal. Security is not option **Vulnerability:** The CLI executor proxy lacked scheme validation for the user-controlled `remoteHost`, creating an SSRF vulnerability. The OAuth callback forwarder lacked prefix validation for `targetBase`, creating an open redirect vulnerability. **Learning:** Taint analysis revealed that input passed from management configurations or remote host specifications must be strictly validated before being used in HTTP requests or redirect targets to prevent protocol abuse and unauthorized redirects. **Prevention:** Always enforce strict protocol validation (e.g., scheme is 'http' or 'https') for outbound requests and validate redirect targets against allowed prefixes (e.g., 'http://localhost' or '/'). +## 2026-11-11 - Fix SSRF via taint analysis in CLI Executor +**Vulnerability:** A Server-Side Request Forgery (SSRF) vulnerability existed in `/app/internal/runtime/executor/cli_executor.go`. When constructing the `/run` endpoint URL for a remote host, the code copied a parsed URL by value (`targetURL := *parsedURL`) and manually cleared fields like `RawPath` and `Fragment`. If an attacker injected a URL with an `Opaque` field (e.g., `http://example.com;opaque=//malicious.com`), the `.String()` method would prioritize `Opaque` over the `Path`, potentially bypassing path constraints and redirecting the request to an unintended server. +**Learning:** Copying parsed `url.URL` structures by value is risky because the `url.URL.String()` function relies on multiple internal fields (like `Opaque`) that can override the standard components (like `Path`). +**Prevention:** Always construct a new `url.URL{}` struct literal explicitly and only copy the known, safe fields (e.g., `Scheme`, `Host`, `Path`, `RawQuery`, `User`) from the untrusted input to ensure predictable URL serialization. diff --git a/cmd/server/main.go b/cmd/server/main.go index 2ddb8216..72b58fa9 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1047,10 +1047,10 @@ func doMemoryReset(cfg *config.Config, opts *MemoryOptions) { // Create automatic backup before reset timestamp := time.Now().Format("20060102-150405") backupPath := fmt.Sprintf("memory-backup-before-reset-%s.tar.gz", timestamp) - + fmt.Println("Creating automatic backup before reset...") fmt.Printf("Backup file: %s\n", backupPath) - + // Create backup if err := createMemoryBackup(cfg, backupPath); err != nil { fmt.Fprintf(os.Stderr, "\nāŒ Error: Failed to create backup: %v\n", err) @@ -1058,7 +1058,7 @@ func doMemoryReset(cfg *config.Config, opts *MemoryOptions) { fmt.Fprintf(os.Stderr, "\nšŸ’” Tip: Ensure you have write permissions and sufficient disk space.\n") os.Exit(1) } - + fmt.Printf("āœ“ Backup created successfully: %s\n\n", backupPath) fmt.Println("Proceeding with reset...") diff --git a/internal/access/config_access/provider.go b/internal/access/config_access/provider.go index 93740eb5..1dfb9b70 100644 --- a/internal/access/config_access/provider.go +++ b/internal/access/config_access/provider.go @@ -54,7 +54,7 @@ func (p *provider) Authenticate(_ context.Context, r *http.Request) (*sdkaccess. if p == nil { return nil, sdkaccess.ErrNotHandled } - + authHeader := r.Header.Get("Authorization") authHeaderGoogle := r.Header.Get("X-Goog-Api-Key") authHeaderAnthropic := r.Header.Get("X-Api-Key") @@ -96,7 +96,7 @@ func (p *provider) Authenticate(_ context.Context, r *http.Request) (*sdkaccess. return &sdkaccess.Result{ Provider: p.Identifier(), Principal: candidate.value, - Metadata: map[string]string{"source": candidate.source}, + Metadata: map[string]string{"source": candidate.source}, }, nil } } @@ -108,13 +108,13 @@ func (p *provider) Authenticate(_ context.Context, r *http.Request) (*sdkaccess. return &sdkaccess.Result{ Provider: p.Identifier(), Principal: firstCandidate, - Metadata: map[string]string{"source": firstSource}, + Metadata: map[string]string{"source": firstSource}, }, nil } return &sdkaccess.Result{ Provider: p.Identifier(), Principal: "anonymous", - Metadata: map[string]string{"source": "none"}, + Metadata: map[string]string{"source": "none"}, }, nil } diff --git a/internal/access/config_access/provider_test.go b/internal/access/config_access/provider_test.go index a1895a63..dbc7e53f 100644 --- a/internal/access/config_access/provider_test.go +++ b/internal/access/config_access/provider_test.go @@ -3,8 +3,8 @@ package configaccess import ( "context" "net/http" - "testing" "net/url" + "testing" sdkaccess "github.com/traylinx/switchAILocal/sdk/access" sdkconfig "github.com/traylinx/switchAILocal/sdk/config" @@ -109,7 +109,7 @@ func TestProvider_Authenticate(t *testing.T) { for k, v := range tt.headers { req.Header.Set(k, v) } - + // Add queryparams q := req.URL.Query() for k, v := range tt.queryParams { diff --git a/internal/api/server.go b/internal/api/server.go index b2899783..c3f60a27 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -397,10 +397,10 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk var autoResolver *autoroute.AutoResolver if cfg.AutoRouting.Enabled { log.Info("Initializing Intelligent Auto-Routing System") - + // 1. Setup Discovery Service discoverySvc := autoroute.NewDiscoveryService(cfg.AutoRouting.Discovery) - + // 2. Register probers based on configured credentials // CLI Probers discoverySvc.RegisterProber(probes.NewCLIProber("geminicli", "gemini")) @@ -412,16 +412,16 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk name := fmt.Sprintf("gemini-api-%d", i) endpoint := "/v1beta/models" if key.ModelsURL != "" { - endpoint = key.ModelsURL + endpoint = key.ModelsURL } discoverySvc.RegisterProber(probes.NewAPIProber(name, key.BaseURL, endpoint, key.APIKey, "Authorization")) } - + for i, key := range cfg.ClaudeKey { name := fmt.Sprintf("claude-api-%d", i) endpoint := "/v1/models" if key.ModelsURL != "" { - endpoint = key.ModelsURL + endpoint = key.ModelsURL } discoverySvc.RegisterProber(probes.NewAPIProber(name, key.BaseURL, endpoint, key.APIKey, "x-api-key")) } @@ -437,30 +437,30 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk // 3. Run Boot-Time Discovery ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - + var candidates []autoroute.CandidateInput if cfg.AutoRouting.Discovery.ProbeOnStartup { log.Info("Running boot-time provider discovery") results := discoverySvc.DiscoverAll(ctx) - + // 4. Convert discovery results to CandidateInputs for provider, res := range results { if !res.Available { log.WithField("provider", provider).Warn("Provider unavailable during discovery") continue } - + for _, model := range res.Models { - candidates = append(candidates, autoroute.CandidateInput{ - Provider: provider, - Model: model.ID, - Available: true, - Latency: res.Latency, - SuccessRate: 1.0, - QuotaHealth: 1.0, - }) + candidates = append(candidates, autoroute.CandidateInput{ + Provider: provider, + Model: model.ID, + Available: true, + Latency: res.Latency, + SuccessRate: 1.0, + QuotaHealth: 1.0, + }) } - + log.WithFields(log.Fields{ "provider": provider, "models": len(res.Models), @@ -468,16 +468,16 @@ func NewServer(cfg *config.Config, authManager *auth.Manager, accessManager *sdk }).Info("Discovered provider models") } } else { - // If probing disabled on boot, just populate basic fallback providers + // If probing disabled on boot, just populate basic fallback providers for _, p := range []string{"geminicli", "claudecli", "codex", "ollama"} { - candidates = append(candidates, autoroute.CandidateInput{ - Provider: p, - Model: "auto", - Available: true, - Latency: 0, + candidates = append(candidates, autoroute.CandidateInput{ + Provider: p, + Model: "auto", + Available: true, + Latency: 0, SuccessRate: 1.0, QuotaHealth: 1.0, - }) + }) } } @@ -595,7 +595,7 @@ func (s *Server) setupRoutes() { if s.cfg != nil { observability.RegisterMetricsRoute(s.engine, s.cfg.Observability.Metrics.Enabled, s.cfg.Observability.Metrics.Path) } - + s.engine.GET("/management", s.serveManagementControlPanel) s.engine.GET("/management.html", s.serveManagementControlPanel) openaiHandlers := openai.NewOpenAIAPIHandler(s.handlers) @@ -1454,7 +1454,7 @@ func AuthMiddleware(manager *sdkaccess.Manager) gin.HandlerFunc { if len(result.Metadata) > 0 { c.Set("accessMetadata", result.Metadata) } - + hasher := sha256.New() hasher.Write([]byte(result.Principal)) c.Set("api_key_hash", "sha256:"+hex.EncodeToString(hasher.Sum(nil))) diff --git a/internal/api/server_coordinator_test.go b/internal/api/server_coordinator_test.go index ca4c51f1..6c2eaf02 100644 --- a/internal/api/server_coordinator_test.go +++ b/internal/api/server_coordinator_test.go @@ -216,7 +216,7 @@ func TestCreatePipelineIntegratorFromCoordinator_ValidCoordinator(t *testing.T) integrator := createPipelineIntegratorFromCoordinator(mockCoordinator) assert.NotNil(t, integrator) - + // Verify the integrator can be used model, messages, err := integrator.ApplySteering(nil, []map[string]string{}) assert.NoError(t, err) @@ -244,7 +244,7 @@ func TestCreateEventBusIntegratorFromCoordinator_ValidCoordinator(t *testing.T) integrator := createEventBusIntegratorFromCoordinator(mockCoordinator) assert.NotNil(t, integrator) - + // Verify the integrator can be used err := integrator.ConnectHeartbeatEvents() assert.NoError(t, err) diff --git a/internal/api/statebox_handler_test.go b/internal/api/statebox_handler_test.go index eee4165a..6c73ff69 100644 --- a/internal/api/statebox_handler_test.go +++ b/internal/api/statebox_handler_test.go @@ -19,19 +19,19 @@ import ( func TestStateBoxStatusHandler_Success(t *testing.T) { // Create a temporary directory for testing tempDir := t.TempDir() - + // Set environment variables os.Setenv("SWITCHAI_STATE_DIR", tempDir) os.Setenv("SWITCHAI_READONLY", "0") defer os.Unsetenv("SWITCHAI_STATE_DIR") defer os.Unsetenv("SWITCHAI_READONLY") - + // Create StateBox sb, err := util.NewStateBox() if err != nil { t.Fatalf("Failed to create StateBox: %v", err) } - + // Create discovery and intelligence directories discoveryDir := filepath.Join(tempDir, "discovery") intelligenceDir := filepath.Join(tempDir, "intelligence") @@ -41,73 +41,73 @@ func TestStateBoxStatusHandler_Success(t *testing.T) { if err := os.MkdirAll(intelligenceDir, 0700); err != nil { t.Fatalf("Failed to create intelligence directory: %v", err) } - + // Create test files registryPath := filepath.Join(discoveryDir, "registry.json") if err := os.WriteFile(registryPath, []byte(`{"test": "data"}`), 0600); err != nil { t.Fatalf("Failed to create registry file: %v", err) } - + dbPath := filepath.Join(intelligenceDir, "feedback.db") if err := os.WriteFile(dbPath, []byte("test db"), 0600); err != nil { t.Fatalf("Failed to create database file: %v", err) } - + // Setup Gin gin.SetMode(gin.TestMode) router := gin.New() router.GET("/api/state-box/status", StateBoxStatusHandler(sb)) - + // Create request req, err := http.NewRequest("GET", "/api/state-box/status", nil) if err != nil { t.Fatalf("Failed to create request: %v", err) } - + // Record response w := httptest.NewRecorder() router.ServeHTTP(w, req) - + // Check status code if w.Code != http.StatusOK { t.Errorf("Expected status code %d, got %d", http.StatusOK, w.Code) } - + // Parse response var status StateBoxStatus if err := json.Unmarshal(w.Body.Bytes(), &status); err != nil { t.Fatalf("Failed to parse response: %v", err) } - + // Verify response if status.RootPath != tempDir { t.Errorf("Expected root path %s, got %s", tempDir, status.RootPath) } - + if status.ReadOnly { t.Error("Expected read-only to be false") } - + if !status.Initialized { t.Error("Expected initialized to be true") } - + if status.DiscoveryRegistry == nil { t.Fatal("Expected discovery registry status to be present") } - + if !status.DiscoveryRegistry.Exists { t.Error("Expected discovery registry to exist") } - + if status.FeedbackDatabase == nil { t.Fatal("Expected feedback database status to be present") } - + if !status.FeedbackDatabase.Exists { t.Error("Expected feedback database to exist") } - + if status.PermissionStatus != "ok" { t.Errorf("Expected permission status 'ok', got '%s'", status.PermissionStatus) } @@ -116,45 +116,45 @@ func TestStateBoxStatusHandler_Success(t *testing.T) { func TestStateBoxStatusHandler_ReadOnlyMode(t *testing.T) { // Create a temporary directory for testing tempDir := t.TempDir() - + // Set environment variables for read-only mode os.Setenv("SWITCHAI_STATE_DIR", tempDir) os.Setenv("SWITCHAI_READONLY", "1") defer os.Unsetenv("SWITCHAI_STATE_DIR") defer os.Unsetenv("SWITCHAI_READONLY") - + // Create StateBox sb, err := util.NewStateBox() if err != nil { t.Fatalf("Failed to create StateBox: %v", err) } - + // Setup Gin gin.SetMode(gin.TestMode) router := gin.New() router.GET("/api/state-box/status", StateBoxStatusHandler(sb)) - + // Create request req, err := http.NewRequest("GET", "/api/state-box/status", nil) if err != nil { t.Fatalf("Failed to create request: %v", err) } - + // Record response w := httptest.NewRecorder() router.ServeHTTP(w, req) - + // Check status code if w.Code != http.StatusOK { t.Errorf("Expected status code %d, got %d", http.StatusOK, w.Code) } - + // Parse response var status StateBoxStatus if err := json.Unmarshal(w.Body.Bytes(), &status); err != nil { t.Fatalf("Failed to parse response: %v", err) } - + // Verify read-only mode if !status.ReadOnly { t.Error("Expected read-only to be true") @@ -166,17 +166,17 @@ func TestStateBoxStatusHandler_NilStateBox(t *testing.T) { gin.SetMode(gin.TestMode) router := gin.New() router.GET("/api/state-box/status", StateBoxStatusHandler(nil)) - + // Create request req, err := http.NewRequest("GET", "/api/state-box/status", nil) if err != nil { t.Fatalf("Failed to create request: %v", err) } - + // Record response w := httptest.NewRecorder() router.ServeHTTP(w, req) - + // Check status code if w.Code != http.StatusServiceUnavailable { t.Errorf("Expected status code %d, got %d", http.StatusServiceUnavailable, w.Code) diff --git a/internal/autoroute/backoff_test.go b/internal/autoroute/backoff_test.go index 379ed858..c7290b8b 100644 --- a/internal/autoroute/backoff_test.go +++ b/internal/autoroute/backoff_test.go @@ -24,8 +24,8 @@ func TestComputeCooldownBackoff_BoundedAndMonotonicUntilCap(t *testing.T) { {3, 40_000}, {4, 80_000}, {5, 160_000}, - {6, 300_000}, // 320s → capped to 300s - {7, 300_000}, // capped + {6, 300_000}, // 320s → capped to 300s + {7, 300_000}, // capped {99, 300_000}, // capped } for _, tt := range cases { diff --git a/internal/autoroute/config.go b/internal/autoroute/config.go index d6515f0c..18d57e30 100644 --- a/internal/autoroute/config.go +++ b/internal/autoroute/config.go @@ -7,15 +7,15 @@ import ( // Config represents the master configuration for the Auto-Routing subsystem. type Config struct { - Enabled bool `yaml:"enabled" json:"enabled"` - MaxResolution time.Duration `yaml:"max-resolution-ms" json:"max_resolution_ms"` - Providers map[string]ProviderTierConfig `yaml:"providers" json:"providers"` - Preferences []ModelPreference `yaml:"preferences" json:"preferences"` - Conservation ConservationConfig `yaml:"conservation" json:"conservation"` - Discovery DiscoveryConfig `yaml:"discovery" json:"discovery"` - Weights ScoringWeights `yaml:"weights" json:"weights"` - IntentMatrix map[string][]string `yaml:"intent-matrix" json:"intent_matrix"` - Lab LabConfig `yaml:"lab" json:"lab"` + Enabled bool `yaml:"enabled" json:"enabled"` + MaxResolution time.Duration `yaml:"max-resolution-ms" json:"max_resolution_ms"` + Providers map[string]ProviderTierConfig `yaml:"providers" json:"providers"` + Preferences []ModelPreference `yaml:"preferences" json:"preferences"` + Conservation ConservationConfig `yaml:"conservation" json:"conservation"` + Discovery DiscoveryConfig `yaml:"discovery" json:"discovery"` + Weights ScoringWeights `yaml:"weights" json:"weights"` + IntentMatrix map[string][]string `yaml:"intent-matrix" json:"intent_matrix"` + Lab LabConfig `yaml:"lab" json:"lab"` // DisabledProviders lists provider names that should never be selected, even if // they score highest. Use when credits are exhausted or a provider is known-bad. // Example: [anthropic] skips all Anthropic models until the list is cleared. @@ -90,8 +90,8 @@ type ModelPreference struct { // ConservationConfig dictates how aggressively the router should hoard premium tokens. type ConservationConfig struct { - Enabled bool `yaml:"enabled" json:"enabled"` - SimpleThreshold int `yaml:"simple-threshold-tokens" json:"simple_threshold_tokens"` + Enabled bool `yaml:"enabled" json:"enabled"` + SimpleThreshold int `yaml:"simple-threshold-tokens" json:"simple_threshold_tokens"` PremiumConservationAt float64 `yaml:"premium-conservation-at" json:"premium_conservation_at"` // percentage (0.0 to 1.0) } @@ -107,10 +107,10 @@ type DiscoveryConfig struct { // LabConfig controls the autonomous self-optimization engine (autoresearch plugin equivalent). type LabConfig struct { - Enabled bool `yaml:"enabled" json:"enabled"` - AdaptationInterval time.Duration `yaml:"adaptation-interval" json:"adaptation_interval"` - MaxWeightDrift float64 `yaml:"max-weight-drift" json:"max_weight_drift"` - MinObservationWindow int `yaml:"min-observation-window" json:"min_observation_window"` // min requests before evaluating (default 10) + Enabled bool `yaml:"enabled" json:"enabled"` + AdaptationInterval time.Duration `yaml:"adaptation-interval" json:"adaptation_interval"` + MaxWeightDrift float64 `yaml:"max-weight-drift" json:"max_weight_drift"` + MinObservationWindow int `yaml:"min-observation-window" json:"min_observation_window"` // min requests before evaluating (default 10) } // ScoringWeights represent the importance of different health metrics. diff --git a/internal/autoroute/discovery.go b/internal/autoroute/discovery.go index 63d45a44..de74f842 100644 --- a/internal/autoroute/discovery.go +++ b/internal/autoroute/discovery.go @@ -60,11 +60,11 @@ type ProviderProber interface { // DiscoveryService orchestrates parallel health and tier probing type DiscoveryService struct { - config DiscoveryConfig - probers []ProviderProber - cache map[string]ProbeResult - cacheMu sync.RWMutex - lastRun time.Time + config DiscoveryConfig + probers []ProviderProber + cache map[string]ProbeResult + cacheMu sync.RWMutex + lastRun time.Time } // NewDiscoveryService initializes the orchestrator with configured probers diff --git a/internal/autoroute/e2e_failover_test.go b/internal/autoroute/e2e_failover_test.go index bca746ea..f17deeb4 100644 --- a/internal/autoroute/e2e_failover_test.go +++ b/internal/autoroute/e2e_failover_test.go @@ -15,7 +15,7 @@ func TestE2EFailover_QuotaExhaustion(t *testing.T) { cfg.IntentMatrix = map[string][]string{ "reasoning": {"openai:o1-preview", "claude:claude-3-opus", "switchai:deepseek-reasoner"}, } - + resolver := NewAutoResolver(cfg, t.TempDir()) req := &RoutingRequest{ @@ -69,7 +69,7 @@ func TestE2EFailover_QuotaExhaustion(t *testing.T) { if decision.SelectedModel == "openai:o1-preview" { t.Errorf("Router erroneously selected the quota-exhausted model instead of failing over.") } - + if decision.SelectedModel == "ollama:llama3" { t.Errorf("Router selected a model that violates the intent matrix restrictions.") } @@ -79,7 +79,7 @@ func TestE2EFailover_QuotaExhaustion(t *testing.T) { t.Errorf("Expected failover to switchai:deepseek-reasoner, got: %s", decision.SelectedModel) } - // The exhausted model should still be in the fallback chain in case the primary fails, + // The exhausted model should still be in the fallback chain in case the primary fails, // but relegated to the back. foundExhausted := false for _, fallback := range decision.FallbackChain { diff --git a/internal/autoroute/experiment.go b/internal/autoroute/experiment.go index b4ca3953..94084952 100644 --- a/internal/autoroute/experiment.go +++ b/internal/autoroute/experiment.go @@ -19,23 +19,23 @@ const DefaultAltSuccessRQS = 0.85 // It directly implements the "fixed-budget, single-metric, keep-or-discard" // loop inspired by AutoResearch. type Lab struct { - config Config - scorer *ProviderScorer - journal *ExperimentJournal - rng *rand.Rand - + config Config + scorer *ProviderScorer + journal *ExperimentJournal + rng *rand.Rand + // The current hypothesis weights being evaluated in the shadow - mu sync.RWMutex - shadowWeights ScoringWeights + mu sync.RWMutex + shadowWeights ScoringWeights activeHypothesis bool // Metric accumulation for the current observation window - windowProdRQS float64 - windowShadowRQS float64 - windowReqCount int - windowExploredCount int // times shadow picked a different model + windowProdRQS float64 + windowShadowRQS float64 + windowReqCount int + windowExploredCount int // times shadow picked a different model - stopCh chan struct{} + stopCh chan struct{} stopOnce sync.Once } @@ -48,10 +48,10 @@ func NewLab(cfg Config, scorer *ProviderScorer, journal *ExperimentJournal) *Lab stopCh: make(chan struct{}), rng: rand.New(rand.NewSource(time.Now().UnixNano())), } - + // Initialize shadow weights to exactly match production initially lab.shadowWeights = cfg.Weights - + return lab } @@ -153,18 +153,18 @@ func (l *Lab) RecordOutcome(reqID string, intent string, complexity float64, pro } shadowScored := shadowScorer.ScoreAll(candidates, complexity) - + // Log the journal entry entry := JournalEntry{ - Timestamp: time.Now(), - RequestID: reqID, - Intent: intent, - Complexity: complexity, - ProdModel: prodOutcome.Model, - ProdTier: prodOutcome.Tier, - ProdLatency: prodOutcome.Latency, - ProdSuccess: prodOutcome.Success, - ProdRQS: prodRQS, + Timestamp: time.Now(), + RequestID: reqID, + Intent: intent, + Complexity: complexity, + ProdModel: prodOutcome.Model, + ProdTier: prodOutcome.Tier, + ProdLatency: prodOutcome.Latency, + ProdSuccess: prodOutcome.Success, + ProdRQS: prodRQS, WeightAvail: l.scorer.weights.Availability, WeightQuota: l.scorer.weights.Quota, WeightLatency: l.scorer.weights.Latency, @@ -174,7 +174,7 @@ func (l *Lab) RecordOutcome(reqID string, intent string, complexity float64, pro if len(shadowScored) > 0 && shadowScored[0].Available { entry.ShadowModel = shadowScored[0].Model entry.ShadowTier = shadowScored[0].EffectiveTier - + if entry.ShadowModel == entry.ProdModel { entry.ShadowExpectedRQS = prodRQS l.windowShadowRQS += prodRQS @@ -300,10 +300,10 @@ func (l *Lab) generateHypothesisLocked() { // Add random noise [-drift/2, +drift/2] to each weight w := ScoringWeights{ - Availability: curr.Availability + (l.rng.Float64() - 0.5) * drift, - Quota: curr.Quota + (l.rng.Float64() - 0.5) * drift, - Latency: curr.Latency + (l.rng.Float64() - 0.5) * drift, - SuccessRate: curr.SuccessRate + (l.rng.Float64() - 0.5) * drift, + Availability: curr.Availability + (l.rng.Float64()-0.5)*drift, + Quota: curr.Quota + (l.rng.Float64()-0.5)*drift, + Latency: curr.Latency + (l.rng.Float64()-0.5)*drift, + SuccessRate: curr.SuccessRate + (l.rng.Float64()-0.5)*drift, } // Clamp limits 0.05 min diff --git a/internal/autoroute/experiment_test.go b/internal/autoroute/experiment_test.go index 50c666d8..48767fc4 100644 --- a/internal/autoroute/experiment_test.go +++ b/internal/autoroute/experiment_test.go @@ -33,12 +33,12 @@ func TestOptimizationLab_EvaluateAndAdapt_Promote(t *testing.T) { Latency: 0.1, SuccessRate: 0.7, // Heavy bias towards success } - + lab.windowReqCount = 20 // Must be >= 10 to evaluate - + // Set the shadow RQS to be significantly higher than production (Avg 0.60 vs Prod 0.50) - lab.windowProdRQS = 20 * 0.50 - lab.windowShadowRQS = 20 * 0.60 + lab.windowProdRQS = 20 * 0.50 + lab.windowShadowRQS = 20 * 0.60 lab.mu.Unlock() // Force adaptation evaluation @@ -47,7 +47,7 @@ func TestOptimizationLab_EvaluateAndAdapt_Promote(t *testing.T) { // Check if the scorer's weights were updated to the winning shadow configuration updatedWeights := scorer.weights assert.InDelta(t, 0.7, updatedWeights.SuccessRate, 0.001, "Scorer weights should be updated to the winning shadow weights") - + // Verify window was reset lab.mu.RLock() assert.Equal(t, 0, lab.windowReqCount) @@ -80,9 +80,9 @@ func TestOptimizationLab_EvaluateAndAdapt_Discard(t *testing.T) { Latency: 0.1, SuccessRate: 0.7, } - + lab.windowReqCount = 20 - + // Set shadow RQS to be WORSE than production lab.windowProdRQS = 20 * 0.60 lab.windowShadowRQS = 20 * 0.50 diff --git a/internal/autoroute/health.go b/internal/autoroute/health.go index ae915f65..9a83233c 100644 --- a/internal/autoroute/health.go +++ b/internal/autoroute/health.go @@ -192,13 +192,13 @@ func (m *ProviderHealthMonitor) RecordRequestOutcome(provider string, latency ti if rlSnap.Detected { state.QuotaRemaining = rlSnap.QuotaHealth log.WithFields(log.Fields{ - "provider": provider, - "quota_health": rlSnap.QuotaHealth, - "req_remaining": rlSnap.RequestRemaining, - "req_limit": rlSnap.RequestLimit, - "token_remaining": rlSnap.TokenRemaining, - "token_limit": rlSnap.TokenLimit, - "retry_after_sec": rlSnap.RetryAfterSec, + "provider": provider, + "quota_health": rlSnap.QuotaHealth, + "req_remaining": rlSnap.RequestRemaining, + "req_limit": rlSnap.RequestLimit, + "token_remaining": rlSnap.TokenRemaining, + "token_limit": rlSnap.TokenLimit, + "retry_after_sec": rlSnap.RetryAfterSec, }).Debug("Rate-limit headers parsed") } @@ -264,7 +264,7 @@ func (m *ProviderHealthMonitor) syncToResolverLocked() { newState.Latency = st.Latency newState.SuccessRate = st.SuccessRate newState.QuotaHealth = st.QuotaRemaining - + // Publish prometheus health metric observability.ProviderHealthScore.WithLabelValues(c.Provider, st.Status).Set(st.SuccessRate) } diff --git a/internal/autoroute/journal.go b/internal/autoroute/journal.go index 20f133f6..25d63021 100644 --- a/internal/autoroute/journal.go +++ b/internal/autoroute/journal.go @@ -14,40 +14,40 @@ import ( // ExperimentJournal maintains a TSV audit trail of all routing // decisions and shadow experiments, directly inspired by AutoResearch. type ExperimentJournal struct { - filePath string - file *os.File - writer *csv.Writer - mu sync.Mutex - enabled bool + filePath string + file *os.File + writer *csv.Writer + mu sync.Mutex + enabled bool // In-memory ring buffer for recent UI telemetry - recent []JournalEntry - recentIdx int - recentLen int - maxRecent int + recent []JournalEntry + recentIdx int + recentLen int + maxRecent int } // JournalEntry represents a single row in the TSV log. type JournalEntry struct { - Timestamp time.Time - RequestID string - Intent string - Complexity float64 + Timestamp time.Time + RequestID string + Intent string + Complexity float64 // Production Decision - ProdModel string - ProdTier string - ProdLatency time.Duration - ProdSuccess bool - ProdRQS float64 + ProdModel string + ProdTier string + ProdLatency time.Duration + ProdSuccess bool + ProdRQS float64 // Shadow Decision (if diff from Prod) - ShadowModel string - ShadowTier string + ShadowModel string + ShadowTier string ShadowExpectedRQS float64 // Predicted RQS based on heuristic/history // Active Weights - WeightAvail float64 - WeightQuota float64 - WeightLatency float64 - WeightSuccess float64 + WeightAvail float64 + WeightQuota float64 + WeightLatency float64 + WeightSuccess float64 } // NewExperimentJournal initializes an append-only TSV logger. @@ -62,7 +62,7 @@ func NewExperimentJournal(workspaceDir string, enabled bool) (*ExperimentJournal } filePath := filepath.Join(logDir, "experiments.tsv") - + // Open file in append mode, create if not exist file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { @@ -128,14 +128,14 @@ func (j *ExperimentJournal) Record(entry JournalEntry) { j.mu.Lock() defer j.mu.Unlock() - + // Update in-memory ring buffer j.recent[j.recentIdx] = entry j.recentIdx = (j.recentIdx + 1) % j.maxRecent if j.recentLen < j.maxRecent { j.recentLen++ } - + if err := j.writer.Write(row); err != nil { log.WithError(err).Warn("Failed to write to experiment journal") return @@ -171,7 +171,7 @@ func (j *ExperimentJournal) GetRecent(n int) []JournalEntry { } result := make([]JournalEntry, n) - + // Read backwards from the current index idx := j.recentIdx - 1 for i := 0; i < n; i++ { @@ -181,6 +181,6 @@ func (j *ExperimentJournal) GetRecent(n int) []JournalEntry { result[i] = j.recent[idx] idx-- } - + return result } diff --git a/internal/autoroute/probes/api_probe.go b/internal/autoroute/probes/api_probe.go index a799a7ab..a510691e 100644 --- a/internal/autoroute/probes/api_probe.go +++ b/internal/autoroute/probes/api_probe.go @@ -75,10 +75,10 @@ func (p *APIProber) Probe(ctx context.Context) autoroute.ProbeResult { if resp.StatusCode >= 200 && resp.StatusCode < 300 { result.Available = true - + // Parse rate limits from headers result.RateLimits = extractRateLimits(p.providerName, resp.Header) - + // Attempt to parse models if it's a known format (e.g. OpenAI /models format) var jsonBody map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&jsonBody); err == nil { diff --git a/internal/autoroute/probes/ollama_probe.go b/internal/autoroute/probes/ollama_probe.go index 54ecf599..e17a40e4 100644 --- a/internal/autoroute/probes/ollama_probe.go +++ b/internal/autoroute/probes/ollama_probe.go @@ -66,7 +66,7 @@ func (p *OllamaProber) Probe(ctx context.Context) autoroute.ProbeResult { if resp.StatusCode == 200 { result.Available = true - + // Parse local models var responseData struct { Models []struct { diff --git a/internal/autoroute/resolver.go b/internal/autoroute/resolver.go index 5fb3096f..5bf736b8 100644 --- a/internal/autoroute/resolver.go +++ b/internal/autoroute/resolver.go @@ -50,7 +50,7 @@ func NewAutoResolver(cfg Config, workspaceDir string) *AutoResolver { scorer: NewProviderScorer(cfg), } r.monitor = NewProviderHealthMonitor(r, 60*time.Second) - + if cfg.Lab.Enabled { journal, err := NewExperimentJournal(workspaceDir, true) if err != nil { @@ -63,7 +63,7 @@ func NewAutoResolver(cfg Config, workspaceDir string) *AutoResolver { } r.lab = NewLab(cfg, r.scorer, journal) } - + return r } @@ -73,7 +73,7 @@ func (r *AutoResolver) SeedCandidates(candidates []CandidateInput) { r.candidatesMu.Lock() r.candidates = candidates r.candidatesMu.Unlock() - + if r.monitor != nil { r.monitor.RegisterInitialCandidates(candidates) } @@ -115,16 +115,16 @@ func (r *AutoResolver) RecordOutcome(reqID string, decision *RoutingDecision, pr if r.monitor != nil { r.monitor.RecordRequestOutcome(provider, latency, success, httpCode, headers) } - + if r.lab != nil && decision != nil { outcome := RequestOutcome{ - Timestamp: time.Now(), - Model: decision.SelectedModel, - Provider: provider, - Tier: GetEffectiveTier(decision.SelectedModel, provider, r.config), - Latency: latency, - Success: success, - StatusCode: httpCode, + Timestamp: time.Now(), + Model: decision.SelectedModel, + Provider: provider, + Tier: GetEffectiveTier(decision.SelectedModel, provider, r.config), + Latency: latency, + Success: success, + StatusCode: httpCode, EstimatedComplexity: decision.EstimatedComplexity, } r.lab.RecordOutcome(reqID, decision.Intent, decision.EstimatedComplexity, decision, outcome) diff --git a/internal/autoroute/rqs.go b/internal/autoroute/rqs.go index d3dd0deb..6f15548d 100644 --- a/internal/autoroute/rqs.go +++ b/internal/autoroute/rqs.go @@ -6,23 +6,23 @@ import ( // RequestOutcome represents the real-world result of a routing decision. type RequestOutcome struct { - Timestamp time.Time - Model string - Provider string - Tier string - Latency time.Duration - Success bool - StatusCode int + Timestamp time.Time + Model string + Provider string + Tier string + Latency time.Duration + Success bool + StatusCode int EstimatedComplexity float64 - TokensUsed int // If available + TokensUsed int // If available } // RQSWeightConfig defines the importance of different factors in the // Routing Quality Score (RQS). This is the master "val_bpb" metric. type RQSWeightConfig struct { - Success float64 - Latency float64 - Efficiency float64 + Success float64 + Latency float64 + Efficiency float64 Conservation float64 } @@ -110,7 +110,7 @@ func calculateEfficiency(tier string, complexity float64) float64 { // calculateConservation specifically rewards saving premium tokens on simple tasks. func calculateConservation(tier string, complexity float64) float64 { isSimple := complexity < 0.4 - + if isSimple { if tier == TierFree || tier == TierLocal { return 1.0 // Maximum conservation achieved diff --git a/internal/autoroute/rqs_test.go b/internal/autoroute/rqs_test.go index ae13e205..ea99a621 100644 --- a/internal/autoroute/rqs_test.go +++ b/internal/autoroute/rqs_test.go @@ -30,9 +30,9 @@ func TestCalculateRQS(t *testing.T) { t.Run("Failed Request", func(t *testing.T) { outcome := RequestOutcome{ - Success: false, - Latency: 5 * time.Second, - Tier: TierPremium, + Success: false, + Latency: 5 * time.Second, + Tier: TierPremium, } score := CalculateRQS(outcome, weights) diff --git a/internal/autoroute/scorer_test.go b/internal/autoroute/scorer_test.go index f0d31d0f..04d87e39 100644 --- a/internal/autoroute/scorer_test.go +++ b/internal/autoroute/scorer_test.go @@ -17,9 +17,9 @@ func newTestConfig() Config { SuccessRate: 0.20, }, Providers: map[string]ProviderTierConfig{ - "claudecli": {Tier: TierPremium}, - "geminicli": {Tier: TierStandard}, - "ollama": {Tier: TierFree, ModelTiers: map[string]string{ + "claudecli": {Tier: TierPremium}, + "geminicli": {Tier: TierStandard}, + "ollama": {Tier: TierFree, ModelTiers: map[string]string{ "kimi-k2.5:cloud": TierStandard, "gpt-oss:120b-cloud": TierStandard, }}, @@ -269,8 +269,8 @@ func TestScorerColdStart(t *testing.T) { func TestScorerTieBreaking(t *testing.T) { // Two candidates with identical scores → lower latency wins cfg := Config{ - Enabled: true, - Weights: ScoringWeights{Availability: 0.25, Quota: 0.25, Latency: 0.25, SuccessRate: 0.25}, + Enabled: true, + Weights: ScoringWeights{Availability: 0.25, Quota: 0.25, Latency: 0.25, SuccessRate: 0.25}, Conservation: ConservationConfig{Enabled: false}, } scorer := NewProviderScorer(cfg) @@ -291,8 +291,8 @@ func TestScorerTieBreaking(t *testing.T) { func TestScorerTieBreaking_Alphabetical(t *testing.T) { // Completely identical scores + latency → alphabetical wins cfg := Config{ - Enabled: true, - Weights: ScoringWeights{Availability: 0.25, Quota: 0.25, Latency: 0.25, SuccessRate: 0.25}, + Enabled: true, + Weights: ScoringWeights{Availability: 0.25, Quota: 0.25, Latency: 0.25, SuccessRate: 0.25}, Conservation: ConservationConfig{Enabled: false}, } scorer := NewProviderScorer(cfg) @@ -340,11 +340,11 @@ func TestLatencyToScore(t *testing.T) { d time.Duration want float64 }{ - {0, 0.5}, // Unknown → neutral - {1 * time.Millisecond, 1.0}, // Almost instant - {2500 * time.Millisecond, 0.5}, // Half max - {5000 * time.Millisecond, 0.0}, // At max - {10000 * time.Millisecond, 0.0}, // Beyond max + {0, 0.5}, // Unknown → neutral + {1 * time.Millisecond, 1.0}, // Almost instant + {2500 * time.Millisecond, 0.5}, // Half max + {5000 * time.Millisecond, 0.0}, // At max + {10000 * time.Millisecond, 0.0}, // Beyond max } for _, tt := range tests { got := latencyToScore(tt.d) diff --git a/internal/autoroute/types.go b/internal/autoroute/types.go index fe6c7764..b4e3ac22 100644 --- a/internal/autoroute/types.go +++ b/internal/autoroute/types.go @@ -58,4 +58,3 @@ type FallbackEntry struct { Model string `json:"model"` Tier string `json:"tier"` } - diff --git a/internal/config/config_integration_test.go b/internal/config/config_integration_test.go index 6d0a725f..6895bcc8 100644 --- a/internal/config/config_integration_test.go +++ b/internal/config/config_integration_test.go @@ -11,21 +11,21 @@ func TestMemoryConfigDefaults(t *testing.T) { // Create a minimal config file tmpDir := t.TempDir() configFile := filepath.Join(tmpDir, "config.yaml") - + configContent := ` host: "127.0.0.1" port: 8080 ` - + if err := os.WriteFile(configFile, []byte(configContent), 0644); err != nil { t.Fatalf("Failed to write config file: %v", err) } - + cfg, err := LoadConfig(configFile) if err != nil { t.Fatalf("Failed to load config: %v", err) } - + // Verify Memory defaults if cfg.Memory.Enabled { t.Error("Memory should be disabled by default") @@ -48,21 +48,21 @@ port: 8080 func TestSteeringConfigDefaults(t *testing.T) { tmpDir := t.TempDir() configFile := filepath.Join(tmpDir, "config.yaml") - + configContent := ` host: "127.0.0.1" port: 8080 ` - + if err := os.WriteFile(configFile, []byte(configContent), 0644); err != nil { t.Fatalf("Failed to write config file: %v", err) } - + cfg, err := LoadConfig(configFile) if err != nil { t.Fatalf("Failed to load config: %v", err) } - + // Verify Steering defaults if cfg.Steering.Enabled { t.Error("Steering should be disabled by default") @@ -79,21 +79,21 @@ port: 8080 func TestHooksConfigDefaults(t *testing.T) { tmpDir := t.TempDir() configFile := filepath.Join(tmpDir, "config.yaml") - + configContent := ` host: "127.0.0.1" port: 8080 ` - + if err := os.WriteFile(configFile, []byte(configContent), 0644); err != nil { t.Fatalf("Failed to write config file: %v", err) } - + cfg, err := LoadConfig(configFile) if err != nil { t.Fatalf("Failed to load config: %v", err) } - + // Verify Hooks defaults if cfg.Hooks.Enabled { t.Error("Hooks should be disabled by default") @@ -110,7 +110,7 @@ port: 8080 func TestMemoryConfigParsing(t *testing.T) { tmpDir := t.TempDir() configFile := filepath.Join(tmpDir, "config.yaml") - + configContent := ` host: "127.0.0.1" port: 8080 @@ -121,16 +121,16 @@ memory: max-log-size-mb: 50 compression: false ` - + if err := os.WriteFile(configFile, []byte(configContent), 0644); err != nil { t.Fatalf("Failed to write config file: %v", err) } - + cfg, err := LoadConfig(configFile) if err != nil { t.Fatalf("Failed to load config: %v", err) } - + // Verify Memory configuration was parsed if !cfg.Memory.Enabled { t.Error("Memory should be enabled") @@ -153,7 +153,7 @@ memory: func TestSteeringConfigParsing(t *testing.T) { tmpDir := t.TempDir() configFile := filepath.Join(tmpDir, "config.yaml") - + configContent := ` host: "127.0.0.1" port: 8080 @@ -162,16 +162,16 @@ steering: rules-dir: "/custom/steering/rules" hot-reload: false ` - + if err := os.WriteFile(configFile, []byte(configContent), 0644); err != nil { t.Fatalf("Failed to write config file: %v", err) } - + cfg, err := LoadConfig(configFile) if err != nil { t.Fatalf("Failed to load config: %v", err) } - + // Verify Steering configuration was parsed if !cfg.Steering.Enabled { t.Error("Steering should be enabled") @@ -188,7 +188,7 @@ steering: func TestHooksConfigParsing(t *testing.T) { tmpDir := t.TempDir() configFile := filepath.Join(tmpDir, "config.yaml") - + configContent := ` host: "127.0.0.1" port: 8080 @@ -197,16 +197,16 @@ hooks: hooks-dir: "/custom/hooks/path" hot-reload: false ` - + if err := os.WriteFile(configFile, []byte(configContent), 0644); err != nil { t.Fatalf("Failed to write config file: %v", err) } - + cfg, err := LoadConfig(configFile) if err != nil { t.Fatalf("Failed to load config: %v", err) } - + // Verify Hooks configuration was parsed if !cfg.Hooks.Enabled { t.Error("Hooks should be enabled") @@ -312,12 +312,12 @@ func TestSanitizeMemory(t *testing.T) { }, }, } - + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := &Config{Memory: tt.input} cfg.SanitizeMemory() - + if cfg.Memory.BaseDir != tt.expected.BaseDir { t.Errorf("BaseDir: expected '%s', got '%s'", tt.expected.BaseDir, cfg.Memory.BaseDir) } @@ -382,12 +382,12 @@ func TestSanitizeSteering(t *testing.T) { }, }, } - + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := &Config{Steering: tt.input} cfg.SanitizeSteering() - + if cfg.Steering.RulesDir != tt.expected.RulesDir { t.Errorf("RulesDir: expected '%s', got '%s'", tt.expected.RulesDir, cfg.Steering.RulesDir) } @@ -432,12 +432,12 @@ func TestSanitizeHooks(t *testing.T) { }, }, } - + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := &Config{Hooks: tt.input} cfg.SanitizeHooks() - + if cfg.Hooks.HooksDir != tt.expected.HooksDir { t.Errorf("HooksDir: expected '%s', got '%s'", tt.expected.HooksDir, cfg.Hooks.HooksDir) } diff --git a/internal/config/heartbeat.go b/internal/config/heartbeat.go index 11e8f056..d62252f3 100644 --- a/internal/config/heartbeat.go +++ b/internal/config/heartbeat.go @@ -203,12 +203,12 @@ func (cfg *Config) GetHeartbeatInterval() time.Duration { if cfg == nil { return 5 * time.Minute } - + interval, err := time.ParseDuration(cfg.Heartbeat.Interval) if err != nil { return 5 * time.Minute // Default fallback } - + return interval } @@ -217,12 +217,12 @@ func (cfg *Config) GetHeartbeatTimeout() time.Duration { if cfg == nil { return 5 * time.Second } - + timeout, err := time.ParseDuration(cfg.Heartbeat.Timeout) if err != nil { return 5 * time.Second // Default fallback } - + return timeout } @@ -231,12 +231,12 @@ func (cfg *Config) GetHeartbeatRetryDelay() time.Duration { if cfg == nil { return time.Second } - + delay, err := time.ParseDuration(cfg.Heartbeat.RetryDelay) if err != nil { return time.Second // Default fallback } - + return delay } @@ -245,14 +245,14 @@ func (cfg *Config) IsProviderHeartbeatEnabled(provider string) bool { if cfg == nil || !cfg.Heartbeat.Enabled { return false } - + normalizedProvider := strings.ToLower(strings.TrimSpace(provider)) if providerConfig, exists := cfg.Heartbeat.Providers[normalizedProvider]; exists { if providerConfig.Enabled != nil { return *providerConfig.Enabled } } - + // Default to enabled if no provider-specific setting return true } @@ -262,7 +262,7 @@ func (cfg *Config) GetProviderHeartbeatInterval(provider string) time.Duration { if cfg == nil { return 5 * time.Minute } - + normalizedProvider := strings.ToLower(strings.TrimSpace(provider)) if providerConfig, exists := cfg.Heartbeat.Providers[normalizedProvider]; exists { if providerConfig.Interval != "" { @@ -271,7 +271,7 @@ func (cfg *Config) GetProviderHeartbeatInterval(provider string) time.Duration { } } } - + // Fall back to global interval return cfg.GetHeartbeatInterval() } @@ -281,7 +281,7 @@ func (cfg *Config) GetProviderHeartbeatTimeout(provider string) time.Duration { if cfg == nil { return 5 * time.Second } - + normalizedProvider := strings.ToLower(strings.TrimSpace(provider)) if providerConfig, exists := cfg.Heartbeat.Providers[normalizedProvider]; exists { if providerConfig.Timeout != "" { @@ -290,7 +290,7 @@ func (cfg *Config) GetProviderHeartbeatTimeout(provider string) time.Duration { } } } - + // Fall back to global timeout return cfg.GetHeartbeatTimeout() -} \ No newline at end of file +} diff --git a/internal/config/performance_config_test.go b/internal/config/performance_config_test.go index 60b88939..688af83f 100644 --- a/internal/config/performance_config_test.go +++ b/internal/config/performance_config_test.go @@ -26,9 +26,9 @@ func TestProviderTimeouts_Resolve(t *testing.T) { {"openai", 20 * time.Second}, {"anthropic", 60 * time.Second}, {"ollama", 180 * time.Second}, - {"gemini", 30 * time.Second}, // unknown → default - {"", 30 * time.Second}, // empty → default - {"unknown", 30 * time.Second}, // unknown → default + {"gemini", 30 * time.Second}, // unknown → default + {"", 30 * time.Second}, // empty → default + {"unknown", 30 * time.Second}, // unknown → default } for _, tt := range tests { @@ -95,10 +95,10 @@ func TestStreamHealth_ResolveFirstByte(t *testing.T) { provider string want time.Duration }{ - {"minimax", 30 * time.Second}, // override - {"ollama", 60 * time.Second}, // override - {"openai", 15 * time.Second}, // unknown → global default - {"", 15 * time.Second}, // empty → global default + {"minimax", 30 * time.Second}, // override + {"ollama", 60 * time.Second}, // override + {"openai", 15 * time.Second}, // unknown → global default + {"", 15 * time.Second}, // empty → global default } for _, tt := range tests { diff --git a/internal/hooks/eventbus.go b/internal/hooks/eventbus.go index 8b25cb27..bdffc132 100644 --- a/internal/hooks/eventbus.go +++ b/internal/hooks/eventbus.go @@ -113,11 +113,11 @@ func (b *EventBus) PublishAsync(ctx *EventContext) { b.mu.RLock() isShutdown := b.shutdown b.mu.RUnlock() - + if isShutdown { return } - + select { case <-b.ctx.Done(): // Bus is shutting down, ignore event @@ -152,7 +152,7 @@ func (b *EventBus) Shutdown() { b.mu.Lock() b.shutdown = true b.mu.Unlock() - + b.cancel() close(b.eventQueue) }) diff --git a/internal/hooks/eventbus_test.go b/internal/hooks/eventbus_test.go index b78d8841..c76f21c0 100644 --- a/internal/hooks/eventbus_test.go +++ b/internal/hooks/eventbus_test.go @@ -503,4 +503,4 @@ func BenchmarkEventBus_WithFilter(b *testing.B) { for i := 0; i < b.N; i++ { bus.Publish(ctx) } -} \ No newline at end of file +} diff --git a/internal/integration/coordinator_heartbeat_test.go b/internal/integration/coordinator_heartbeat_test.go index 4c30f4e1..1b9223b8 100644 --- a/internal/integration/coordinator_heartbeat_test.go +++ b/internal/integration/coordinator_heartbeat_test.go @@ -13,7 +13,7 @@ import ( func TestServiceCoordinator_HeartbeatWithProviders(t *testing.T) { t.Skip("Skipping test due to deadlock in heartbeat monitor Start() - this is a known issue") - + tmpDir := t.TempDir() // Create a main config with Ollama enabled diff --git a/internal/integration/coordinator_test.go b/internal/integration/coordinator_test.go index e9b557b8..5460d52e 100644 --- a/internal/integration/coordinator_test.go +++ b/internal/integration/coordinator_test.go @@ -204,7 +204,7 @@ func TestServiceCoordinator_WithMemoryEnabled(t *testing.T) { func TestServiceCoordinator_WithHeartbeatEnabled(t *testing.T) { t.Skip("Skipping heartbeat test - requires provider checkers to be registered") - + tmpDir := t.TempDir() cfg := &IntegrationConfig{ @@ -247,7 +247,7 @@ func TestServiceCoordinator_WithHeartbeatEnabled(t *testing.T) { // Start and stop ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - + if err := coordinator.Start(ctx); err != nil { t.Fatalf("Failed to start coordinator: %v", err) } diff --git a/internal/integration/events_test.go b/internal/integration/events_test.go index 46efab08..4c7571e9 100644 --- a/internal/integration/events_test.go +++ b/internal/integration/events_test.go @@ -537,7 +537,7 @@ func TestHeartbeatEventBridgeIgnoresSystemEvents(t *testing.T) { // TestIntegrationWithHeartbeatMonitor tests the full integration with a heartbeat monitor. func TestIntegrationWithHeartbeatMonitor(t *testing.T) { t.Skip("Skipping integration test due to deadlock in heartbeat monitor Start() - this is a known issue in the heartbeat monitor implementation") - + // This test demonstrates the integration works conceptually, but there's a deadlock // in the heartbeat monitor's Start() method when it tries to emit events while holding a lock. // The event handler (heartbeatEventBridge) is called asynchronously but the Start() method diff --git a/internal/integration/handler_adapter.go b/internal/integration/handler_adapter.go index 01846c99..875bc8c3 100644 --- a/internal/integration/handler_adapter.go +++ b/internal/integration/handler_adapter.go @@ -27,12 +27,12 @@ func (ha *HandlerAdapter) ApplySteering(ctx interface{}, messages []map[string]s if ha.integrator == nil { return "", messages, nil } - + routingCtx, ok := ctx.(*steering.RoutingContext) if !ok { return "", messages, nil } - + return ha.integrator.ApplySteering(routingCtx, messages) } @@ -42,18 +42,18 @@ func (ha *HandlerAdapter) RecordRouting(decision interface{}) error { if ha.integrator == nil { return nil } - + // Handle *memory.RoutingDecision directly if routingDecision, ok := decision.(*memory.RoutingDecision); ok { return ha.integrator.RecordRouting(routingDecision) } - + // Handle map[string]interface{} by converting to RoutingDecision if decisionMap, ok := decision.(map[string]interface{}); ok { routingDecision := ha.mapToRoutingDecision(decisionMap) return ha.integrator.RecordRouting(routingDecision) } - + return nil } @@ -63,18 +63,18 @@ func (ha *HandlerAdapter) UpdateOutcome(decision interface{}) error { if ha.integrator == nil { return nil } - + // Handle *memory.RoutingDecision directly if routingDecision, ok := decision.(*memory.RoutingDecision); ok { return ha.integrator.UpdateOutcome(routingDecision) } - + // Handle map[string]interface{} by converting to RoutingDecision if decisionMap, ok := decision.(map[string]interface{}); ok { routingDecision := ha.mapToRoutingDecision(decisionMap) return ha.integrator.UpdateOutcome(routingDecision) } - + return nil } @@ -84,18 +84,18 @@ func (ha *HandlerAdapter) EmitRoutingEvent(decision interface{}) error { if ha.integrator == nil { return nil } - + // Handle *memory.RoutingDecision directly if routingDecision, ok := decision.(*memory.RoutingDecision); ok { return ha.integrator.EmitRoutingEvent(routingDecision) } - + // Handle map[string]interface{} by converting to RoutingDecision if decisionMap, ok := decision.(map[string]interface{}); ok { routingDecision := ha.mapToRoutingDecision(decisionMap) return ha.integrator.EmitRoutingEvent(routingDecision) } - + return nil } @@ -104,17 +104,17 @@ func (ha *HandlerAdapter) mapToRoutingDecision(m map[string]interface{}) *memory decision := &memory.RoutingDecision{ Timestamp: time.Now(), } - + // Extract API key hash if apiKeyHash, ok := m["api_key_hash"].(string); ok { decision.APIKeyHash = apiKeyHash } - + // Extract timestamp if timestamp, ok := m["timestamp"].(time.Time); ok { decision.Timestamp = timestamp } - + // Extract request info if requestMap, ok := m["request"].(map[string]interface{}); ok { if model, ok := requestMap["model"].(string); ok { @@ -130,7 +130,7 @@ func (ha *HandlerAdapter) mapToRoutingDecision(m map[string]interface{}) *memory decision.Request.ContentLength = contentLength } } - + // Extract routing info if routingMap, ok := m["routing"].(map[string]interface{}); ok { if selectedModel, ok := routingMap["selected_model"].(string); ok { @@ -146,7 +146,7 @@ func (ha *HandlerAdapter) mapToRoutingDecision(m map[string]interface{}) *memory decision.Routing.LatencyMs = latencyMs } } - + // Extract outcome info if outcomeMap, ok := m["outcome"].(map[string]interface{}); ok { if success, ok := outcomeMap["success"].(bool); ok { @@ -162,7 +162,7 @@ func (ha *HandlerAdapter) mapToRoutingDecision(m map[string]interface{}) *memory decision.Outcome.QualityScore = qualityScore } } - + return decision } diff --git a/internal/integration/handler_adapter_test.go b/internal/integration/handler_adapter_test.go index 351b7ffa..2b7b34f8 100644 --- a/internal/integration/handler_adapter_test.go +++ b/internal/integration/handler_adapter_test.go @@ -80,7 +80,7 @@ func TestHandlerAdapter_RecordRouting_WithNilIntegrator(t *testing.T) { func TestHandlerAdapter_EmitRoutingEvent_WithMap(t *testing.T) { // Create a mock integrator with a real event bus emitCalled := false - + // We can't easily mock the event bus, so we'll just verify the method doesn't panic mockIntegrator := &RequestPipelineIntegrator{ eventBus: nil, // Event bus is optional diff --git a/internal/integration/pipeline_test.go b/internal/integration/pipeline_test.go index 366d41e6..08e700a3 100644 --- a/internal/integration/pipeline_test.go +++ b/internal/integration/pipeline_test.go @@ -157,13 +157,13 @@ func TestEmitRoutingEvent_NoEventBus(t *testing.T) { func TestEmitRoutingEvent_WithEventBus(t *testing.T) { eventBus := hooks.NewEventBus() defer eventBus.Shutdown() - + rpi := NewRequestPipelineIntegrator(nil, nil, eventBus) // Subscribe to routing events var eventReceived sync.WaitGroup eventReceived.Add(1) - + eventBus.Subscribe(hooks.EventRoutingDecision, func(ctx *hooks.EventContext) { defer eventReceived.Done() @@ -251,13 +251,13 @@ func TestExtractProvider(t *testing.T) { func TestEmitRoutingEvent_WithError(t *testing.T) { eventBus := hooks.NewEventBus() defer eventBus.Shutdown() - + rpi := NewRequestPipelineIntegrator(nil, nil, eventBus) // Subscribe to routing events var eventReceived sync.WaitGroup eventReceived.Add(1) - + eventBus.Subscribe(hooks.EventRoutingDecision, func(ctx *hooks.EventContext) { defer eventReceived.Done() diff --git a/internal/intelligence/cache/semantic_cache.go b/internal/intelligence/cache/semantic_cache.go index 2c35be87..0f5e9896 100644 --- a/internal/intelligence/cache/semantic_cache.go +++ b/internal/intelligence/cache/semantic_cache.go @@ -147,7 +147,7 @@ func (c *SemanticCache) Lookup(query string) (interface{}, error) { c.mu.Lock() c.metrics.Hits++ c.metrics.AvgHitLatency = time.Since(start) - + // Move to front of LRU list c.lruList.MoveToFront(bestMatch.element) c.mu.Unlock() @@ -259,13 +259,13 @@ func (c *SemanticCache) GetMetrics() CacheMetrics { func (c *SemanticCache) GetMetricsAsMap() map[string]interface{} { metrics := c.GetMetrics() return map[string]interface{}{ - "hits": metrics.Hits, - "misses": metrics.Misses, - "evictions": metrics.Evictions, - "size": metrics.Size, - "avg_hit_latency": metrics.AvgHitLatency.Milliseconds(), - "avg_lookup": metrics.AvgLookup.Milliseconds(), - "hit_rate": c.GetHitRate(), + "hits": metrics.Hits, + "misses": metrics.Misses, + "evictions": metrics.Evictions, + "size": metrics.Size, + "avg_hit_latency": metrics.AvgHitLatency.Milliseconds(), + "avg_lookup": metrics.AvgLookup.Milliseconds(), + "hit_rate": c.GetHitRate(), } } diff --git a/internal/intelligence/capability/analyzer.go b/internal/intelligence/capability/analyzer.go index 789da72c..595d38aa 100644 --- a/internal/intelligence/capability/analyzer.go +++ b/internal/intelligence/capability/analyzer.go @@ -58,37 +58,37 @@ func (a *Analyzer) Analyze(model *DiscoveredModel) *ModelCapability { } capability := &ModelCapability{} - + // Normalize model name for pattern matching modelName := strings.ToLower(model.ID) displayName := strings.ToLower(model.DisplayName) - + // Detect coding capability capability.SupportsCoding = a.detectCoding(modelName, displayName) - + // Detect reasoning capability capability.SupportsReasoning = a.detectReasoning(modelName, displayName) - + // Detect vision capability capability.SupportsVision = a.detectVision(modelName, displayName) - + // Detect if model is local capability.IsLocal = a.detectLocal(model.Provider) - + // Infer latency tier based on model characteristics capability.EstimatedLatency = a.inferLatency(modelName, displayName) - + // Infer cost tier capability.CostTier = a.inferCostTier(modelName, displayName, capability.IsLocal) - + // Context window inference would require metadata from providers // For now, we set reasonable defaults based on model patterns capability.ContextWindow = a.inferContextWindow(modelName, displayName) - + log.Debugf("Analyzed model %s: coding=%v, reasoning=%v, vision=%v, local=%v", model.ID, capability.SupportsCoding, capability.SupportsReasoning, capability.SupportsVision, capability.IsLocal) - + return capability } @@ -120,13 +120,13 @@ func (a *Analyzer) detectCoding(modelName, displayName string) bool { "starcoder", "codellama", } - + for _, pattern := range codingPatterns { if strings.Contains(modelName, pattern) || strings.Contains(displayName, pattern) { return true } } - + return false } @@ -143,13 +143,13 @@ func (a *Analyzer) detectReasoning(modelName, displayName string) bool { "reasoning", "think", } - + for _, pattern := range reasoningPatterns { if strings.Contains(modelName, pattern) || strings.Contains(displayName, pattern) { return true } } - + return false } @@ -167,13 +167,13 @@ func (a *Analyzer) detectVision(modelName, displayName string) bool { "llava", "multimodal", } - + for _, pattern := range visionPatterns { if strings.Contains(modelName, pattern) || strings.Contains(displayName, pattern) { return true } } - + return false } @@ -185,14 +185,14 @@ func (a *Analyzer) detectLocal(provider string) bool { "localai", "llamacpp", } - + providerLower := strings.ToLower(provider) for _, local := range localProviders { if strings.Contains(providerLower, local) { return true } } - + return false } @@ -209,13 +209,13 @@ func (a *Analyzer) inferLatency(modelName, displayName string) string { "1b", "3b", } - + for _, pattern := range fastPatterns { if strings.Contains(modelName, pattern) || strings.Contains(displayName, pattern) { return "fast" } } - + // Slow models (reasoning, large) slowPatterns := []string{ "reasoner", @@ -226,13 +226,13 @@ func (a *Analyzer) inferLatency(modelName, displayName string) string { "70b", "405b", } - + for _, pattern := range slowPatterns { if strings.Contains(modelName, pattern) || strings.Contains(displayName, pattern) { return "slow" } } - + // Default to standard return "standard" } @@ -243,7 +243,7 @@ func (a *Analyzer) inferCostTier(modelName, displayName string, isLocal bool) st if isLocal { return "free" } - + // Cheap models (check before expensive patterns) cheapPatterns := []string{ "mini", @@ -251,13 +251,13 @@ func (a *Analyzer) inferCostTier(modelName, displayName string, isLocal bool) st "flash", "3.5", } - + for _, pattern := range cheapPatterns { if strings.Contains(modelName, pattern) || strings.Contains(displayName, pattern) { return "low" } } - + // Expensive models expensivePatterns := []string{ "opus", @@ -265,13 +265,13 @@ func (a *Analyzer) inferCostTier(modelName, displayName string, isLocal bool) st "o3", "claude-4", } - + for _, pattern := range expensivePatterns { if strings.Contains(modelName, pattern) || strings.Contains(displayName, pattern) { return "high" } } - + // Default to medium return "medium" } @@ -286,23 +286,23 @@ func (a *Analyzer) inferContextWindow(modelName, displayName string) int { if strings.Contains(modelName, "200k") || strings.Contains(displayName, "200k") { return 200000 } - + // Gemini models (check early since "gemini" contains "mini") if strings.Contains(modelName, "gemini") { return 128000 } - + // Check for small model indicators (more specific patterns) // These need to be checked before broader patterns like "gpt-4" or "claude" if strings.Contains(modelName, "-mini") || strings.Contains(modelName, "-haiku") { return 8000 } - + // GPT-4 and Claude models typically have large contexts if strings.Contains(modelName, "gpt-4") || strings.Contains(modelName, "claude") { return 128000 } - + // Default context window return 32000 } diff --git a/internal/intelligence/capability/analyzer_test.go b/internal/intelligence/capability/analyzer_test.go index 81a765ba..135eff87 100644 --- a/internal/intelligence/capability/analyzer_test.go +++ b/internal/intelligence/capability/analyzer_test.go @@ -20,11 +20,11 @@ func TestNewAnalyzer(t *testing.T) { func TestAnalyzeNilModel(t *testing.T) { analyzer := NewAnalyzer() capability := analyzer.Analyze(nil) - + if capability == nil { t.Fatal("Analyze returned nil for nil model") } - + // Should return empty capability if capability.SupportsCoding || capability.SupportsReasoning || capability.SupportsVision { t.Error("Expected empty capability for nil model") @@ -36,12 +36,12 @@ func TestAnalyzeNilModel(t *testing.T) { // "code", "codex", "deepseek", "kimi" func TestDetectCoding(t *testing.T) { analyzer := NewAnalyzer() - + testCases := []struct { - name string - modelID string - displayName string - expectCoding bool + name string + modelID string + displayName string + expectCoding bool }{ {"GPT-4 with code", "gpt-4-code", "GPT-4 Code", true}, {"Codex model", "codex-001", "Codex", true}, @@ -52,7 +52,7 @@ func TestDetectCoding(t *testing.T) { {"Regular chat model", "gpt-3.5-turbo", "GPT-3.5 Turbo", false}, {"Claude Sonnet", "claude-sonnet", "Claude Sonnet", false}, } - + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { model := &DiscoveredModel{ @@ -60,9 +60,9 @@ func TestDetectCoding(t *testing.T) { DisplayName: tc.displayName, Provider: "test", } - + capability := analyzer.Analyze(model) - + if capability.SupportsCoding != tc.expectCoding { t.Errorf("Expected SupportsCoding=%v for %s, got %v", tc.expectCoding, tc.modelID, capability.SupportsCoding) @@ -76,12 +76,12 @@ func TestDetectCoding(t *testing.T) { // "reasoner", "o1", "o3", "thinking", "pro" func TestDetectReasoning(t *testing.T) { analyzer := NewAnalyzer() - + testCases := []struct { - name string - modelID string - displayName string - expectReasoning bool + name string + modelID string + displayName string + expectReasoning bool }{ {"O1 model", "o1-preview", "O1 Preview", true}, {"O3 model", "o3-mini", "O3 Mini", true}, @@ -91,7 +91,7 @@ func TestDetectReasoning(t *testing.T) { {"Regular chat", "gpt-3.5-turbo", "GPT-3.5 Turbo", false}, {"Fast model", "gpt-4o-mini", "GPT-4o Mini", false}, } - + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { model := &DiscoveredModel{ @@ -99,9 +99,9 @@ func TestDetectReasoning(t *testing.T) { DisplayName: tc.displayName, Provider: "test", } - + capability := analyzer.Analyze(model) - + if capability.SupportsReasoning != tc.expectReasoning { t.Errorf("Expected SupportsReasoning=%v for %s, got %v", tc.expectReasoning, tc.modelID, capability.SupportsReasoning) @@ -115,12 +115,12 @@ func TestDetectReasoning(t *testing.T) { // "vision", "4o", "gpt-4", "gemini", "claude-3" func TestDetectVision(t *testing.T) { analyzer := NewAnalyzer() - + testCases := []struct { - name string - modelID string - displayName string - expectVision bool + name string + modelID string + displayName string + expectVision bool }{ {"GPT-4o", "gpt-4o", "GPT-4o", true}, {"GPT-4 Turbo", "gpt-4-turbo", "GPT-4 Turbo", true}, @@ -132,7 +132,7 @@ func TestDetectVision(t *testing.T) { {"Text-only model", "gpt-3.5-turbo", "GPT-3.5 Turbo", false}, {"Llama 3", "llama-3-8b", "Llama 3", false}, } - + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { model := &DiscoveredModel{ @@ -140,9 +140,9 @@ func TestDetectVision(t *testing.T) { DisplayName: tc.displayName, Provider: "test", } - + capability := analyzer.Analyze(model) - + if capability.SupportsVision != tc.expectVision { t.Errorf("Expected SupportsVision=%v for %s, got %v", tc.expectVision, tc.modelID, capability.SupportsVision) @@ -154,7 +154,7 @@ func TestDetectVision(t *testing.T) { // TestDetectLocal tests local model detection func TestDetectLocal(t *testing.T) { analyzer := NewAnalyzer() - + testCases := []struct { name string provider string @@ -168,7 +168,7 @@ func TestDetectLocal(t *testing.T) { {"Anthropic", "anthropic", false}, {"Google", "google", false}, } - + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { model := &DiscoveredModel{ @@ -176,9 +176,9 @@ func TestDetectLocal(t *testing.T) { DisplayName: "Test Model", Provider: tc.provider, } - + capability := analyzer.Analyze(model) - + if capability.IsLocal != tc.expectLocal { t.Errorf("Expected IsLocal=%v for provider %s, got %v", tc.expectLocal, tc.provider, capability.IsLocal) @@ -190,11 +190,11 @@ func TestDetectLocal(t *testing.T) { // TestInferLatency tests latency tier inference func TestInferLatency(t *testing.T) { analyzer := NewAnalyzer() - + testCases := []struct { - name string - modelID string - expectLatency string + name string + modelID string + expectLatency string }{ {"Fast mini model", "gpt-4o-mini", "fast"}, {"Fast turbo model", "gpt-3.5-turbo", "fast"}, @@ -204,7 +204,7 @@ func TestInferLatency(t *testing.T) { {"Slow opus", "claude-opus", "slow"}, {"Standard model", "gpt-4", "standard"}, } - + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { model := &DiscoveredModel{ @@ -212,9 +212,9 @@ func TestInferLatency(t *testing.T) { DisplayName: tc.modelID, Provider: "test", } - + capability := analyzer.Analyze(model) - + if capability.EstimatedLatency != tc.expectLatency { t.Errorf("Expected EstimatedLatency=%s for %s, got %s", tc.expectLatency, tc.modelID, capability.EstimatedLatency) @@ -226,12 +226,12 @@ func TestInferLatency(t *testing.T) { // TestInferCostTier tests cost tier inference func TestInferCostTier(t *testing.T) { analyzer := NewAnalyzer() - + testCases := []struct { - name string - modelID string - provider string - expectCost string + name string + modelID string + provider string + expectCost string }{ {"Local model", "llama-3", "ollama", "free"}, {"Expensive opus", "claude-opus", "anthropic", "high"}, @@ -240,7 +240,7 @@ func TestInferCostTier(t *testing.T) { {"Cheap haiku", "claude-haiku", "anthropic", "low"}, {"Medium model", "gpt-4", "openai", "medium"}, } - + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { model := &DiscoveredModel{ @@ -248,9 +248,9 @@ func TestInferCostTier(t *testing.T) { DisplayName: tc.modelID, Provider: tc.provider, } - + capability := analyzer.Analyze(model) - + if capability.CostTier != tc.expectCost { t.Errorf("Expected CostTier=%s for %s, got %s", tc.expectCost, tc.modelID, capability.CostTier) @@ -263,11 +263,11 @@ func TestInferCostTier(t *testing.T) { // Requirements 2.5: Extract context window size from model metadata when available func TestInferContextWindow(t *testing.T) { analyzer := NewAnalyzer() - + testCases := []struct { - name string - modelID string - expectWindow int + name string + modelID string + expectWindow int }{ {"128k model", "gpt-4-128k", 128000}, {"200k model", "claude-200k", 200000}, @@ -278,7 +278,7 @@ func TestInferContextWindow(t *testing.T) { {"Haiku model", "claude-haiku", 8000}, {"Default model", "llama-3", 32000}, } - + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { model := &DiscoveredModel{ @@ -286,9 +286,9 @@ func TestInferContextWindow(t *testing.T) { DisplayName: tc.modelID, Provider: "test", } - + capability := analyzer.Analyze(model) - + if capability.ContextWindow != tc.expectWindow { t.Errorf("Expected ContextWindow=%d for %s, got %d", tc.expectWindow, tc.modelID, capability.ContextWindow) @@ -300,7 +300,7 @@ func TestInferContextWindow(t *testing.T) { // TestAnalyzeBatch tests batch analysis of multiple models func TestAnalyzeBatch(t *testing.T) { analyzer := NewAnalyzer() - + models := []*DiscoveredModel{ { ID: "gpt-4-code", @@ -318,23 +318,23 @@ func TestAnalyzeBatch(t *testing.T) { Provider: "ollama", }, } - + capabilities := analyzer.AnalyzeBatch(models) - + if len(capabilities) != len(models) { t.Errorf("Expected %d capabilities, got %d", len(models), len(capabilities)) } - + // Verify first model (coding) if !capabilities[0].SupportsCoding { t.Error("Expected first model to support coding") } - + // Verify second model (vision) if !capabilities[1].SupportsVision { t.Error("Expected second model to support vision") } - + // Verify third model (local) if !capabilities[2].IsLocal { t.Error("Expected third model to be local") @@ -344,29 +344,29 @@ func TestAnalyzeBatch(t *testing.T) { // TestMultipleCapabilities tests models with multiple capabilities func TestMultipleCapabilities(t *testing.T) { analyzer := NewAnalyzer() - + // GPT-4o should have coding, reasoning, and vision model := &DiscoveredModel{ ID: "gpt-4o", DisplayName: "GPT-4o", Provider: "openai", } - + capability := analyzer.Analyze(model) - + if !capability.SupportsVision { t.Error("Expected GPT-4o to support vision") } - + // Claude 3 Opus should have vision model = &DiscoveredModel{ ID: "claude-3-opus", DisplayName: "Claude 3 Opus", Provider: "anthropic", } - + capability = analyzer.Analyze(model) - + if !capability.SupportsVision { t.Error("Expected Claude 3 Opus to support vision") } @@ -375,7 +375,7 @@ func TestMultipleCapabilities(t *testing.T) { // TestCaseInsensitivity tests that pattern matching is case-insensitive func TestCaseInsensitivity(t *testing.T) { analyzer := NewAnalyzer() - + testCases := []struct { modelID string displayName string @@ -384,16 +384,16 @@ func TestCaseInsensitivity(t *testing.T) { {"gpt-4-code", "gpt-4 code"}, {"GpT-4-CoDe", "GpT-4 CoDe"}, } - + for _, tc := range testCases { model := &DiscoveredModel{ ID: tc.modelID, DisplayName: tc.displayName, Provider: "test", } - + capability := analyzer.Analyze(model) - + if !capability.SupportsCoding { t.Errorf("Expected coding support for %s (case-insensitive)", tc.modelID) } diff --git a/internal/intelligence/cascade/quality_signals.go b/internal/intelligence/cascade/quality_signals.go index 34363482..95cbf5c7 100644 --- a/internal/intelligence/cascade/quality_signals.go +++ b/internal/intelligence/cascade/quality_signals.go @@ -61,12 +61,12 @@ type QualitySignalDetector struct { func NewQualitySignalDetector() *QualitySignalDetector { return &QualitySignalDetector{ abruptEndingPatterns: []*regexp.Regexp{ - regexp.MustCompile(`\.\.\.$`), // Ends with ... - regexp.MustCompile(`(?i)(?:and|but|or|so|then)\s*$`), // Ends with conjunction - regexp.MustCompile(`(?i)(?:the|a|an|this|that)\s*$`), // Ends with article - regexp.MustCompile(`(?i)(?:to|for|with|from|in)\s*$`), // Ends with preposition - regexp.MustCompile(`(?i)(?:is|are|was|were|be)\s*$`), // Ends with verb - regexp.MustCompile(`(?i)(?:I|we|you|they|it)\s*$`), // Ends with pronoun + regexp.MustCompile(`\.\.\.$`), // Ends with ... + regexp.MustCompile(`(?i)(?:and|but|or|so|then)\s*$`), // Ends with conjunction + regexp.MustCompile(`(?i)(?:the|a|an|this|that)\s*$`), // Ends with article + regexp.MustCompile(`(?i)(?:to|for|with|from|in)\s*$`), // Ends with preposition + regexp.MustCompile(`(?i)(?:is|are|was|were|be)\s*$`), // Ends with verb + regexp.MustCompile(`(?i)(?:I|we|you|they|it)\s*$`), // Ends with pronoun regexp.MustCompile(`(?i)(?:can|will|would|should)\s*$`), // Ends with modal }, truncationPatterns: []*regexp.Regexp{ @@ -85,8 +85,8 @@ func NewQualitySignalDetector() *QualitySignalDetector { codeBlockPattern: regexp.MustCompile("```"), incompleteCodePatterns: []*regexp.Regexp{ regexp.MustCompile(`(?m)^\s*(?:def|func|function|class|if|for|while|switch)\s+[^{]*$`), // Unclosed block - regexp.MustCompile(`(?m)\{\s*$`), // Opening brace at end - regexp.MustCompile(`(?m)^\s*//\s*\.\.\.\s*$`), // Comment with ... + regexp.MustCompile(`(?m)\{\s*$`), // Opening brace at end + regexp.MustCompile(`(?m)^\s*//\s*\.\.\.\s*$`), // Comment with ... }, } } diff --git a/internal/intelligence/discovery/capability_integration_test.go b/internal/intelligence/discovery/capability_integration_test.go index ba82ff02..d7e9591a 100644 --- a/internal/intelligence/discovery/capability_integration_test.go +++ b/internal/intelligence/discovery/capability_integration_test.go @@ -15,30 +15,30 @@ import ( // integrated into the discovery service and written to JSON. func TestCapabilityIntegration(t *testing.T) { tmpDir := t.TempDir() - + // Create discovery service svc, err := NewService(tmpDir, nil) if err != nil { t.Fatalf("Failed to create discovery service: %v", err) } - + // Run discovery ctx := context.Background() err = svc.DiscoverAll(ctx) if err != nil { t.Logf("Discovery completed with error (expected if no providers): %v", err) } - + // Get models models := svc.GetAvailableModels() - + // Verify capabilities are populated for _, model := range models { if model.Capabilities == nil { t.Errorf("Model %s has nil capabilities", model.ID) continue } - + // Verify capability fields exist (values depend on model name) t.Logf("Model %s capabilities: coding=%v, reasoning=%v, vision=%v, local=%v", model.ID, @@ -47,20 +47,20 @@ func TestCapabilityIntegration(t *testing.T) { model.Capabilities.SupportsVision, model.Capabilities.IsLocal) } - + // Write registry registryPath := tmpDir + "/test_registry.json" err = svc.WriteRegistry(registryPath) if err != nil { t.Fatalf("Failed to write registry: %v", err) } - + // Read and verify JSON structure data, err := os.ReadFile(registryPath) if err != nil { t.Fatalf("Failed to read registry file: %v", err) } - + var registry struct { Models []struct { ID string `json:"id"` @@ -75,19 +75,19 @@ func TestCapabilityIntegration(t *testing.T) { } `json:"capabilities"` } `json:"models"` } - + err = json.Unmarshal(data, ®istry) if err != nil { t.Fatalf("Failed to unmarshal registry JSON: %v", err) } - + // Verify capabilities are in JSON for _, model := range registry.Models { if model.Capabilities == nil { t.Errorf("Model %s has nil capabilities in JSON", model.ID) continue } - + // Verify all capability fields are present if model.Capabilities.ContextWindow == 0 { t.Errorf("Model %s has zero context window", model.ID) @@ -98,9 +98,9 @@ func TestCapabilityIntegration(t *testing.T) { if model.Capabilities.CostTier == "" { t.Errorf("Model %s has empty cost tier", model.ID) } - + t.Logf("āœ“ Model %s has complete capability data in JSON", model.ID) } - + t.Logf("āœ“ Verified %d models have capability data in JSON", len(registry.Models)) } diff --git a/internal/intelligence/discovery/integration_test.go b/internal/intelligence/discovery/integration_test.go index 48cf4654..b7687a27 100644 --- a/internal/intelligence/discovery/integration_test.go +++ b/internal/intelligence/discovery/integration_test.go @@ -15,69 +15,69 @@ import ( // TestCheckpoint_DiscoveryRunsOnStartup verifies that discovery runs when enabled func TestCheckpoint_DiscoveryRunsOnStartup(t *testing.T) { tmpDir := t.TempDir() - + svc, err := NewService(tmpDir, nil) if err != nil { t.Fatalf("Failed to create service: %v", err) } - + ctx := context.Background() - + // Run discovery (simulating startup) err = svc.DiscoverAll(ctx) if err != nil { t.Logf("Discovery completed with error (acceptable): %v", err) } - + // Verify models were discovered models := svc.GetAvailableModels() if models == nil { t.Fatal("GetAvailableModels returned nil") } - + t.Logf("Discovery completed: %d models discovered", len(models)) } // TestCheckpoint_JSONFileCreated verifies that available_models.json is created func TestCheckpoint_JSONFileCreated(t *testing.T) { tmpDir := t.TempDir() - + svc, err := NewService(tmpDir, nil) if err != nil { t.Fatalf("Failed to create service: %v", err) } - + ctx := context.Background() - + // Run discovery err = svc.DiscoverAll(ctx) if err != nil { t.Logf("Discovery completed with error (acceptable): %v", err) } - + // Write registry err = svc.WriteRegistry("") if err != nil { t.Fatalf("Failed to write registry: %v", err) } - + // Verify JSON file exists registryPath := filepath.Join(tmpDir, "available_models.json") if _, err := os.Stat(registryPath); os.IsNotExist(err) { t.Fatal("available_models.json was not created") } - + // Verify JSON is valid data, err := os.ReadFile(registryPath) if err != nil { t.Fatalf("Failed to read registry file: %v", err) } - + var registry DiscoveryRegistry if err := json.Unmarshal(data, ®istry); err != nil { t.Fatalf("Registry file contains invalid JSON: %v", err) } - + // Verify structure if registry.Providers == nil { t.Error("Registry missing providers field") @@ -88,31 +88,31 @@ func TestCheckpoint_JSONFileCreated(t *testing.T) { if registry.GeneratedAt.IsZero() { t.Error("Registry missing generated_at timestamp") } - - t.Logf("Registry file created successfully with %d models from %d providers", + + t.Logf("Registry file created successfully with %d models from %d providers", registry.TotalModels, len(registry.Providers)) } // TestCheckpoint_LuaAPIReturnsModelsOrError verifies Lua API behavior func TestCheckpoint_LuaAPIReturnsModelsOrError(t *testing.T) { tmpDir := t.TempDir() - + svc, err := NewService(tmpDir, nil) if err != nil { t.Fatalf("Failed to create service: %v", err) } - + ctx := context.Background() - + // Run discovery _ = svc.DiscoverAll(ctx) - + // Test GetAvailableModelsAsMap (used by Lua API) modelsMap := svc.GetAvailableModelsAsMap() if modelsMap == nil { t.Fatal("GetAvailableModelsAsMap returned nil") } - + // Verify each model has required fields for Lua for i, model := range modelsMap { if _, ok := model["id"]; !ok { @@ -128,6 +128,6 @@ func TestCheckpoint_LuaAPIReturnsModelsOrError(t *testing.T) { t.Errorf("Model %d missing 'discovered_at' field", i) } } - + t.Logf("Lua API compatibility verified: %d models available", len(modelsMap)) } diff --git a/internal/intelligence/discovery/service_test.go b/internal/intelligence/discovery/service_test.go index e6bfdd0a..7bad1952 100644 --- a/internal/intelligence/discovery/service_test.go +++ b/internal/intelligence/discovery/service_test.go @@ -15,16 +15,16 @@ import ( // TestNewService tests the creation of a new discovery service func TestNewService(t *testing.T) { tmpDir := t.TempDir() - + svc, err := NewService(tmpDir, nil) if err != nil { t.Fatalf("Failed to create service: %v", err) } - + if svc == nil { t.Fatal("Service is nil") } - + if svc.cacheDir != tmpDir { t.Errorf("Expected cacheDir %s, got %s", tmpDir, svc.cacheDir) } @@ -36,16 +36,16 @@ func TestNewServiceWithHomeDir(t *testing.T) { if err != nil { t.Fatalf("Failed to create service with home dir: %v", err) } - + if svc == nil { t.Fatal("Service is nil") } - + // Verify home directory was expanded if svc.cacheDir[0] == '~' { t.Error("Home directory was not expanded") } - + // Cleanup os.RemoveAll(svc.cacheDir) } @@ -53,20 +53,20 @@ func TestNewServiceWithHomeDir(t *testing.T) { // TestDiscoverAllTimeout tests that discovery respects the 5-second timeout func TestDiscoverAllTimeout(t *testing.T) { tmpDir := t.TempDir() - + svc, err := NewService(tmpDir, nil) if err != nil { t.Fatalf("Failed to create service: %v", err) } - + ctx := context.Background() start := time.Now() - + // Run discovery (may fail due to no providers, but should complete quickly) _ = svc.DiscoverAll(ctx) - + duration := time.Since(start) - + // Should complete within 6 seconds (5s timeout + 1s buffer) if duration > 6*time.Second { t.Errorf("Discovery took too long: %v (expected < 6s)", duration) @@ -76,29 +76,29 @@ func TestDiscoverAllTimeout(t *testing.T) { // TestDiscoverAllGracefulFailure tests that discovery handles provider failures gracefully func TestDiscoverAllGracefulFailure(t *testing.T) { tmpDir := t.TempDir() - + svc, err := NewService(tmpDir, nil) if err != nil { t.Fatalf("Failed to create service: %v", err) } - + ctx := context.Background() - + // Run discovery - should not panic even if providers fail err = svc.DiscoverAll(ctx) - + // Error is acceptable (no providers configured), but should not panic // We just verify it completes without crashing if err != nil { t.Logf("Discovery completed with expected error: %v", err) } - + // Verify we can still get results (even if empty) models := svc.GetAvailableModels() if models == nil { t.Error("GetAvailableModels returned nil") } - + providers := svc.GetProviderStatus() if providers == nil { t.Error("GetProviderStatus returned nil") @@ -108,22 +108,22 @@ func TestDiscoverAllGracefulFailure(t *testing.T) { // TestGetAvailableModels tests retrieving discovered models func TestGetAvailableModels(t *testing.T) { tmpDir := t.TempDir() - + svc, err := NewService(tmpDir, nil) if err != nil { t.Fatalf("Failed to create service: %v", err) } - + // Initially should be empty models := svc.GetAvailableModels() if len(models) != 0 { t.Errorf("Expected 0 models initially, got %d", len(models)) } - + // After discovery, should return results (even if empty due to no providers) ctx := context.Background() _ = svc.DiscoverAll(ctx) - + models = svc.GetAvailableModels() if models == nil { t.Error("GetAvailableModels returned nil after discovery") @@ -133,22 +133,22 @@ func TestGetAvailableModels(t *testing.T) { // TestGetAvailableModelsAsMap tests the map conversion for Lua interop func TestGetAvailableModelsAsMap(t *testing.T) { tmpDir := t.TempDir() - + svc, err := NewService(tmpDir, nil) if err != nil { t.Fatalf("Failed to create service: %v", err) } - + // Run discovery ctx := context.Background() _ = svc.DiscoverAll(ctx) - + // Get models as map modelsMap := svc.GetAvailableModelsAsMap() if modelsMap == nil { t.Error("GetAvailableModelsAsMap returned nil") } - + // Verify structure of returned maps for _, model := range modelsMap { // Check required fields exist @@ -167,34 +167,34 @@ func TestGetAvailableModelsAsMap(t *testing.T) { // TestWriteRegistry tests writing the discovery registry to disk func TestWriteRegistry(t *testing.T) { tmpDir := t.TempDir() - + svc, err := NewService(tmpDir, nil) if err != nil { t.Fatalf("Failed to create service: %v", err) } - + // Run discovery ctx := context.Background() _ = svc.DiscoverAll(ctx) - + // Write registry with default path err = svc.WriteRegistry("") if err != nil { t.Fatalf("Failed to write registry: %v", err) } - + // Verify file was created registryPath := filepath.Join(tmpDir, "available_models.json") if _, err := os.Stat(registryPath); os.IsNotExist(err) { t.Error("Registry file was not created") } - + // Verify file is valid JSON data, err := os.ReadFile(registryPath) if err != nil { t.Fatalf("Failed to read registry file: %v", err) } - + if len(data) == 0 { t.Error("Registry file is empty") } @@ -203,23 +203,23 @@ func TestWriteRegistry(t *testing.T) { // TestWriteRegistryCustomPath tests writing registry to a custom path func TestWriteRegistryCustomPath(t *testing.T) { tmpDir := t.TempDir() - + svc, err := NewService(tmpDir, nil) if err != nil { t.Fatalf("Failed to create service: %v", err) } - + // Run discovery ctx := context.Background() _ = svc.DiscoverAll(ctx) - + // Write registry to custom path customPath := filepath.Join(tmpDir, "custom_models.json") err = svc.WriteRegistry(customPath) if err != nil { t.Fatalf("Failed to write registry to custom path: %v", err) } - + // Verify file was created at custom path if _, err := os.Stat(customPath); os.IsNotExist(err) { t.Error("Registry file was not created at custom path") @@ -229,12 +229,12 @@ func TestWriteRegistryCustomPath(t *testing.T) { // TestShutdown tests graceful shutdown func TestShutdown(t *testing.T) { tmpDir := t.TempDir() - + svc, err := NewService(tmpDir, nil) if err != nil { t.Fatalf("Failed to create service: %v", err) } - + ctx := context.Background() err = svc.Shutdown(ctx) if err != nil { @@ -245,16 +245,16 @@ func TestShutdown(t *testing.T) { // TestConcurrentAccess tests thread-safe access to models func TestConcurrentAccess(t *testing.T) { tmpDir := t.TempDir() - + svc, err := NewService(tmpDir, nil) if err != nil { t.Fatalf("Failed to create service: %v", err) } - + // Run discovery ctx := context.Background() _ = svc.DiscoverAll(ctx) - + // Spawn multiple goroutines to access models concurrently done := make(chan bool) for i := 0; i < 10; i++ { @@ -267,7 +267,7 @@ func TestConcurrentAccess(t *testing.T) { done <- true }() } - + // Wait for all goroutines to complete for i := 0; i < 10; i++ { <-done diff --git a/internal/intelligence/embedding/locator_test.go b/internal/intelligence/embedding/locator_test.go index 74f0eced..743852bf 100644 --- a/internal/intelligence/embedding/locator_test.go +++ b/internal/intelligence/embedding/locator_test.go @@ -96,7 +96,7 @@ func TestGetSharedLibraryPath(t *testing.T) { // This may or may not find a library depending on the system // Just verify the function doesn't panic path := locator.GetSharedLibraryPath() - + // If a path is returned, it should be a valid file if path != "" { _, err := os.Stat(path) diff --git a/internal/intelligence/embedding/tokenizer_test.go b/internal/intelligence/embedding/tokenizer_test.go index 4e05492c..9914b1c8 100644 --- a/internal/intelligence/embedding/tokenizer_test.go +++ b/internal/intelligence/embedding/tokenizer_test.go @@ -95,9 +95,9 @@ func TestTokenizeWord(t *testing.T) { require.NoError(t, err) tests := []struct { - name string - word string - wantUNK bool // whether we expect UNK tokens + name string + word string + wantUNK bool // whether we expect UNK tokens }{ { name: "known word", diff --git a/internal/intelligence/feedback/collector_test.go b/internal/intelligence/feedback/collector_test.go index 259b2550..02598ef2 100644 --- a/internal/intelligence/feedback/collector_test.go +++ b/internal/intelligence/feedback/collector_test.go @@ -444,43 +444,43 @@ func TestCollectorTimestamp(t *testing.T) { func TestCollectorWithStateBox(t *testing.T) { // Create temporary directory for StateBox tmpDir := t.TempDir() - + // Set up environment for StateBox os.Setenv("SWITCHAI_STATE_DIR", tmpDir) defer os.Unsetenv("SWITCHAI_STATE_DIR") - + // Create StateBox sb, err := NewStateBox() if err != nil { t.Fatalf("Failed to create StateBox: %v", err) } - + // Create collector with just a filename collector, err := NewCollector("feedback.db", 90) if err != nil { t.Fatalf("NewCollector() failed: %v", err) } - + // Set StateBox collector.SetStateBox(sb) - + ctx := context.Background() if err := collector.Initialize(ctx); err != nil { t.Fatalf("Initialize() failed: %v", err) } defer func() { _ = collector.Shutdown(ctx) }() - + // Verify database is in the intelligence directory expectedPath := filepath.Join(tmpDir, "intelligence", "feedback.db") if collector.dbPath != expectedPath { t.Errorf("Expected dbPath %s, got %s", expectedPath, collector.dbPath) } - + // Verify database file was created if _, err := os.Stat(expectedPath); os.IsNotExist(err) { t.Error("Database file was not created in StateBox intelligence directory") } - + // Verify directory has correct permissions (0700) info, err := os.Stat(filepath.Dir(expectedPath)) if err != nil { @@ -489,7 +489,7 @@ func TestCollectorWithStateBox(t *testing.T) { if info.Mode().Perm() != 0700 { t.Errorf("Expected directory permissions 0700, got %o", info.Mode().Perm()) } - + // Test recording works record := &FeedbackRecord{ Query: "Test query", @@ -499,17 +499,17 @@ func TestCollectorWithStateBox(t *testing.T) { LatencyMs: 100, Success: true, } - + if err := collector.Record(ctx, record); err != nil { t.Fatalf("Record() failed: %v", err) } - + // Verify record was stored records, err := collector.GetRecent(ctx, 1) if err != nil { t.Fatalf("GetRecent() failed: %v", err) } - + if len(records) != 1 { t.Fatalf("Expected 1 record, got %d", len(records)) } @@ -519,30 +519,30 @@ func TestCollectorWithStateBox(t *testing.T) { func TestCollectorReadOnlyMode(t *testing.T) { // Create temporary directory for StateBox tmpDir := t.TempDir() - + // Set up environment for StateBox (read-write first) os.Setenv("SWITCHAI_STATE_DIR", tmpDir) defer os.Unsetenv("SWITCHAI_STATE_DIR") - + // Create StateBox in read-write mode sb, err := NewStateBox() if err != nil { t.Fatalf("Failed to create StateBox: %v", err) } - + // Create collector and initialize in read-write mode collector, err := NewCollector("feedback.db", 90) if err != nil { t.Fatalf("NewCollector() failed: %v", err) } - + collector.SetStateBox(sb) - + ctx := context.Background() if err := collector.Initialize(ctx); err != nil { t.Fatalf("Initialize() failed: %v", err) } - + // Record some data record := &FeedbackRecord{ Query: "Test query", @@ -552,54 +552,54 @@ func TestCollectorReadOnlyMode(t *testing.T) { LatencyMs: 100, Success: true, } - + if err := collector.Record(ctx, record); err != nil { t.Fatalf("Record() failed: %v", err) } - + // Shutdown if err := collector.Shutdown(ctx); err != nil { t.Fatalf("Shutdown() failed: %v", err) } - + // Now switch to read-only mode os.Setenv("SWITCHAI_READONLY", "1") defer os.Unsetenv("SWITCHAI_READONLY") - + // Create new StateBox in read-only mode sbReadOnly, err := NewStateBox() if err != nil { t.Fatalf("Failed to create read-only StateBox: %v", err) } - + if !sbReadOnly.IsReadOnly() { t.Fatal("StateBox should be in read-only mode") } - + // Create new collector collectorRO, err := NewCollector("feedback.db", 90) if err != nil { t.Fatalf("NewCollector() failed: %v", err) } - + collectorRO.SetStateBox(sbReadOnly) - + // Initialize in read-only mode if err := collectorRO.Initialize(ctx); err != nil { t.Fatalf("Initialize() in read-only mode failed: %v", err) } defer func() { _ = collectorRO.Shutdown(ctx) }() - + // Verify we can read existing data records, err := collectorRO.GetRecent(ctx, 10) if err != nil { t.Fatalf("GetRecent() failed in read-only mode: %v", err) } - + if len(records) != 1 { t.Fatalf("Expected 1 record, got %d", len(records)) } - + // Verify we cannot write in read-only mode newRecord := &FeedbackRecord{ Query: "Another query", @@ -609,12 +609,12 @@ func TestCollectorReadOnlyMode(t *testing.T) { LatencyMs: 100, Success: true, } - + err = collectorRO.Record(ctx, newRecord) if err == nil { t.Fatal("Record() should fail in read-only mode") } - + // Verify the error is ErrReadOnlyMode if err.Error() != "Read-only environment: write operations disabled" { t.Errorf("Expected ErrReadOnlyMode, got: %v", err) diff --git a/internal/intelligence/learning_integration_test.go b/internal/intelligence/learning_integration_test.go index 397462ac..8e3c5f91 100644 --- a/internal/intelligence/learning_integration_test.go +++ b/internal/intelligence/learning_integration_test.go @@ -120,18 +120,18 @@ func TestLearningIntegration_ConfidenceAdjustment(t *testing.T) { } adjustedConfidence := router.adjustConfidenceWithMemory(decision) - + // Expected: 0.7 (base) + 0.15 (preference) + 0.03 (bias) + 0.1 (time) = 0.98 assert.Greater(t, adjustedConfidence, 0.9, "Should boost confidence with positive factors") assert.LessOrEqual(t, adjustedConfidence, 1.0, "Should clamp to maximum 1.0") // Test 2: Confidence adjustment with negative bias decision.SelectedModel = "openai:gpt-4" - decision.Intent = "general" // No learned preference + decision.Intent = "general" // No learned preference decision.Timestamp = time.Date(2024, 1, 1, 23, 0, 0, 0, time.UTC) // 11 PM - off hours adjustedConfidence = router.adjustConfidenceWithMemory(decision) - + // Expected: 0.7 (base) + 0.0 (no preference) + (-0.05) (negative bias) + 0.0 (no time match) = 0.65 assert.Less(t, adjustedConfidence, 0.7, "Should reduce confidence with negative bias") assert.Greater(t, adjustedConfidence, 0.6, "Should not reduce too much") @@ -160,13 +160,13 @@ func TestLearningIntegration_TimePatternMatching(t *testing.T) { hour int expected bool }{ - {"coding", 14, true}, // 2 PM - work hours - {"coding", 2, false}, // 2 AM - off hours - {"chat", 20, true}, // 8 PM - evening - {"chat", 10, false}, // 10 AM - work hours - {"reasoning", 12, true}, // 12 PM - focused hours + {"coding", 14, true}, // 2 PM - work hours + {"coding", 2, false}, // 2 AM - off hours + {"chat", 20, true}, // 8 PM - evening + {"chat", 10, false}, // 10 AM - work hours + {"reasoning", 12, true}, // 12 PM - focused hours {"reasoning", 22, false}, // 10 PM - late - {"unknown", 14, false}, // Unknown intent + {"unknown", 14, false}, // Unknown intent } for _, tc := range testCases { @@ -178,7 +178,7 @@ func TestLearningIntegration_TimePatternMatching(t *testing.T) { } isMatch := router.isTimePatternMatch(decision, prefs) - assert.Equal(t, tc.expected, isMatch, + assert.Equal(t, tc.expected, isMatch, "Time pattern match for %s at %d:00 should be %t", tc.intent, tc.hour, tc.expected) }) } @@ -228,7 +228,7 @@ func TestLearningIntegration_EndToEndRouting(t *testing.T) { assert.Equal(t, "claude:3.5", decision.SelectedModel, "Should use learned preference") assert.True(t, decision.UsedMemory, "Should indicate memory was used") assert.Equal(t, "preferences", decision.MemorySource, "Should indicate preferences source") - + // Confidence should be adjusted by learning algorithm assert.Greater(t, decision.Confidence, 0.85, "Should have high confidence with learning adjustment") } @@ -272,7 +272,7 @@ func TestLearningIntegration_ReflexTierWithLearning(t *testing.T) { assert.Equal(t, "reflex", decision.Tier, "Should use reflex routing tier") assert.Equal(t, "coding", decision.Intent, "Should classify as coding") assert.Equal(t, "claudecli:claude-sonnet-4", decision.SelectedModel, "Should use reflex model choice") - + // Confidence should be higher than base due to positive provider bias assert.Greater(t, decision.Confidence, 0.90, "Should have boosted confidence due to positive bias") } @@ -304,7 +304,7 @@ func TestLearningIntegration_OutcomeRecording(t *testing.T) { // Test successful outcome outcome := router.CreateOutcome(decision, true, 1500, nil) - + assert.NotNil(t, outcome) assert.Equal(t, decision, outcome.Decision) assert.True(t, outcome.Success) @@ -392,4 +392,4 @@ func BenchmarkLearningIntegration_EndToEndRouting(b *testing.B) { for i := 0; i < b.N; i++ { _, _ = router.Route(ctx, request) } -} \ No newline at end of file +} diff --git a/internal/intelligence/matrix/builder.go b/internal/intelligence/matrix/builder.go index 1ed7d7be..49fc6b42 100644 --- a/internal/intelligence/matrix/builder.go +++ b/internal/intelligence/matrix/builder.go @@ -90,7 +90,6 @@ func NewBuilder(preferLocal, costOptimization bool, overrides map[string]string) } } - // Build generates a dynamic matrix from the provided models with capabilities. // It scores and ranks models for each capability slot and assigns primary models // with fallback chains. @@ -312,7 +311,6 @@ func (b *Builder) scoreCreative(cap *capability.ModelCapability) float64 { return score } - // scoreFast scores a model for the fast slot. // Prioritizes: low latency, low cost func (b *Builder) scoreFast(cap *capability.ModelCapability) float64 { @@ -369,7 +367,7 @@ func (b *Builder) scoreSecure(cap *capability.ModelCapability) float64 { if contextScore > 1.0 { contextScore = 1.0 } - + if cap.IsLocal { score += contextScore * 0.1 } else { diff --git a/internal/intelligence/matrix/builder_test.go b/internal/intelligence/matrix/builder_test.go index 48d4d196..ebb63370 100644 --- a/internal/intelligence/matrix/builder_test.go +++ b/internal/intelligence/matrix/builder_test.go @@ -82,7 +82,6 @@ func TestScoringCodingSlot(t *testing.T) { } } - // TestScoringReasoningSlot tests that reasoning models are scored correctly func TestScoringReasoningSlot(t *testing.T) { builder := NewBuilder(false, false, nil) @@ -134,30 +133,30 @@ func TestFallbackChainGeneration(t *testing.T) { ID: "model-1", Provider: "openai", Capabilities: &capability.ModelCapability{ - SupportsCoding: true, - ContextWindow: 128000, - EstimatedLatency: "standard", - CostTier: "high", + SupportsCoding: true, + ContextWindow: 128000, + EstimatedLatency: "standard", + CostTier: "high", }, }, { ID: "model-2", Provider: "openai", Capabilities: &capability.ModelCapability{ - SupportsCoding: true, - ContextWindow: 64000, - EstimatedLatency: "standard", - CostTier: "medium", + SupportsCoding: true, + ContextWindow: 64000, + EstimatedLatency: "standard", + CostTier: "medium", }, }, { ID: "model-3", Provider: "openai", Capabilities: &capability.ModelCapability{ - SupportsCoding: true, - ContextWindow: 32000, - EstimatedLatency: "fast", - CostTier: "low", + SupportsCoding: true, + ContextWindow: 32000, + EstimatedLatency: "fast", + CostTier: "low", }, }, } @@ -232,10 +231,10 @@ func TestManualOverrideApplication(t *testing.T) { ID: "deepseek-coder", Provider: "openai", Capabilities: &capability.ModelCapability{ - SupportsCoding: true, - ContextWindow: 128000, - EstimatedLatency: "standard", - CostTier: "medium", + SupportsCoding: true, + ContextWindow: 128000, + EstimatedLatency: "standard", + CostTier: "medium", }, }, } @@ -290,20 +289,20 @@ func TestCostOptimization(t *testing.T) { ID: "expensive-model", Provider: "openai", Capabilities: &capability.ModelCapability{ - SupportsCoding: true, - ContextWindow: 128000, - EstimatedLatency: "standard", - CostTier: "high", + SupportsCoding: true, + ContextWindow: 128000, + EstimatedLatency: "standard", + CostTier: "high", }, }, { ID: "cheap-model", Provider: "openai", Capabilities: &capability.ModelCapability{ - SupportsCoding: true, - ContextWindow: 64000, - EstimatedLatency: "standard", - CostTier: "low", + SupportsCoding: true, + ContextWindow: 64000, + EstimatedLatency: "standard", + CostTier: "low", }, }, } diff --git a/internal/intelligence/router.go b/internal/intelligence/router.go index fc457445..e2f61c9c 100644 --- a/internal/intelligence/router.go +++ b/internal/intelligence/router.go @@ -26,8 +26,8 @@ import ( // It provides three routing tiers: Reflex (fast rules), Semantic (intent matching), // and Cognitive (LLM classification), with learned preferences from memory. type CortexRouter struct { - config *config.IntelligenceConfig - memoryManager memory.MemoryManager + config *config.IntelligenceConfig + memoryManager memory.MemoryManager learningEngine *learning.LearningEngine // Services from intelligence service @@ -271,10 +271,10 @@ func (cr *CortexRouter) tryMemoryRouting(ctx context.Context, request *RoutingRe decision.UsedMemory = true decision.MemorySource = "cache" decision.Reason = "Found cached routing decision" - + // Apply learning-based confidence adjustment decision.Confidence = cr.adjustConfidenceWithMemory(decision) - + return decision } } @@ -292,10 +292,10 @@ func (cr *CortexRouter) tryMemoryRouting(ctx context.Context, request *RoutingRe decision.UsedMemory = true decision.MemorySource = "preferences" decision.Reason = fmt.Sprintf("Using learned preference for intent '%s'", intent) - + // Apply learning-based confidence adjustment decision.Confidence = cr.adjustConfidenceWithMemory(decision) - + return decision } } @@ -316,12 +316,12 @@ func (cr *CortexRouter) tryReflexTier(ctx context.Context, request *RoutingReque decision.SelectedModel = "ollama:qwen:0.5b" // Route to local model for privacy decision.Confidence = 0.99 decision.Reason = "PII detected, routing to local model for privacy" - + // Apply learning-based confidence adjustment if cr.memoryEnabled { decision.Confidence = cr.adjustConfidenceWithMemory(decision) } - + return decision } @@ -334,12 +334,12 @@ func (cr *CortexRouter) tryReflexTier(ctx context.Context, request *RoutingReque decision.SelectedModel = "ollama:qwen:0.5b" // Fast local model for simple queries decision.Confidence = 0.95 decision.Reason = "Simple greeting detected" - + // Apply learning-based confidence adjustment if cr.memoryEnabled { decision.Confidence = cr.adjustConfidenceWithMemory(decision) } - + return decision } @@ -352,12 +352,12 @@ func (cr *CortexRouter) tryReflexTier(ctx context.Context, request *RoutingReque decision.SelectedModel = "claudecli:claude-sonnet-4" // Best for coding decision.Confidence = 0.90 decision.Reason = "Code patterns detected" - + // Apply learning-based confidence adjustment if cr.memoryEnabled { decision.Confidence = cr.adjustConfidenceWithMemory(decision) } - + return decision } @@ -370,12 +370,12 @@ func (cr *CortexRouter) tryReflexTier(ctx context.Context, request *RoutingReque decision.SelectedModel = "geminicli:gemini-2.5-pro" // Good for reasoning decision.Confidence = 0.85 decision.Reason = "Mathematical reasoning patterns detected" - + // Apply learning-based confidence adjustment if cr.memoryEnabled { decision.Confidence = cr.adjustConfidenceWithMemory(decision) } - + return decision } @@ -680,9 +680,9 @@ func (cr *CortexRouter) isTimePatternMatch(decision *RoutingDecision, prefs *mem // This is a simplified implementation - in a full implementation, // we would check the time patterns from the learning system // For now, we'll use a basic heuristic based on time of day - + hour := decision.Timestamp.Hour() - + // Simple time-based patterns (could be enhanced with actual learned patterns) switch decision.Intent { case "coding": diff --git a/internal/intelligence/semantic/tier.go b/internal/intelligence/semantic/tier.go index c2870f5d..c760fad4 100644 --- a/internal/intelligence/semantic/tier.go +++ b/internal/intelligence/semantic/tier.go @@ -49,7 +49,6 @@ type MatchResult struct { LatencyMs int64 `json:"latency_ms"` } - // EmbeddingEngine defines the interface for computing embeddings. // This allows the semantic tier to work with the embedding engine. type EmbeddingEngine interface { @@ -82,8 +81,8 @@ type Tier struct { mu sync.RWMutex // metrics for tracking - matchCount int64 - hitCount int64 + matchCount int64 + hitCount int64 totalLatencyMs int64 } @@ -253,7 +252,6 @@ func (t *Tier) MatchIntent(query string) (*MatchResult, error) { }, nil } - // GetIntents returns all loaded intents. // // Returns: @@ -315,13 +313,13 @@ func (t *Tier) GetMetrics() map[string]interface{} { } return map[string]interface{}{ - "match_count": t.matchCount, - "hit_count": t.hitCount, - "hit_rate": hitRate, - "avg_latency_ms": avgLatency, - "intent_count": len(t.intents), - "threshold": t.threshold, - "enabled": t.enabled, + "match_count": t.matchCount, + "hit_count": t.hitCount, + "hit_rate": hitRate, + "avg_latency_ms": avgLatency, + "intent_count": len(t.intents), + "threshold": t.threshold, + "enabled": t.enabled, } } @@ -344,7 +342,6 @@ func (t *Tier) Shutdown() error { return nil } - // MatchIntentInterface matches an intent and returns the result as an interface type. // This is used by the intelligence service to avoid circular imports. func (t *Tier) MatchIntentInterface(query string) (interface{}, error) { diff --git a/internal/intelligence/skills/registry_test.go b/internal/intelligence/skills/registry_test.go index 438fc8a6..618a81b1 100644 --- a/internal/intelligence/skills/registry_test.go +++ b/internal/intelligence/skills/registry_test.go @@ -33,7 +33,7 @@ func TestNewRegistry(t *testing.T) { func TestLoadAll(t *testing.T) { // Create temporary directory with test skills tmpDir := t.TempDir() - + // Create a test skill skillDir := filepath.Join(tmpDir, "test-skill") if err := os.MkdirAll(skillDir, 0755); err != nil { @@ -134,7 +134,7 @@ func TestLoadAllMultipleSkills(t *testing.T) { // TestGetSkill tests retrieving a skill by ID. func TestGetSkill(t *testing.T) { tmpDir := t.TempDir() - + // Create a test skill skillDir := filepath.Join(tmpDir, "test-skill") if err := os.MkdirAll(skillDir, 0755); err != nil { @@ -270,7 +270,7 @@ func (m *MockEmbeddingEngine) CosineSimilarity(a, b []float32) float64 { // TestEmbeddingComputation tests embedding computation during skill loading. func TestEmbeddingComputation(t *testing.T) { tmpDir := t.TempDir() - + // Create a test skill skillDir := filepath.Join(tmpDir, "test-skill") if err := os.MkdirAll(skillDir, 0755); err != nil { @@ -319,7 +319,7 @@ required-capability: coding // TestSkillRetrieval tests skill retrieval methods. func TestSkillRetrieval(t *testing.T) { tmpDir := t.TempDir() - + // Create test skills for i := 1; i <= 3; i++ { skillDir := filepath.Join(tmpDir, "skill-"+string(rune('0'+i))) @@ -342,7 +342,7 @@ func TestSkillRetrieval(t *testing.T) { // Test GetAllSkills returns a copy skills1 := registry.GetAllSkills() - + if len(skills1) != 3 { t.Errorf("expected 3 skills, got %d", len(skills1)) } @@ -358,7 +358,7 @@ func TestSkillRetrieval(t *testing.T) { // TestMatchSkill tests skill matching functionality. func TestMatchSkill(t *testing.T) { tmpDir := t.TempDir() - + // Create test skills with different descriptions skills := []struct { id string @@ -446,7 +446,7 @@ func TestMatchSkillEmptyEmbedding(t *testing.T) { // TestMatchSkillBelowThreshold tests skill matching below confidence threshold. func TestMatchSkillBelowThreshold(t *testing.T) { tmpDir := t.TempDir() - + // Create a test skill skillDir := filepath.Join(tmpDir, "test-skill") if err := os.MkdirAll(skillDir, 0755); err != nil { @@ -501,7 +501,7 @@ required-capability: coding // TestMatchSkillUsageTracking tests that skill matching updates usage statistics. func TestMatchSkillUsageTracking(t *testing.T) { tmpDir := t.TempDir() - + // Create a test skill skillDir := filepath.Join(tmpDir, "test-skill") if err := os.MkdirAll(skillDir, 0755); err != nil { @@ -564,7 +564,7 @@ required-capability: coding // TestMatchSkillWithMultipleSkills tests matching with multiple skills. func TestMatchSkillWithMultipleSkills(t *testing.T) { tmpDir := t.TempDir() - + // Create multiple test skills skills := []struct { id string diff --git a/internal/learning/analysis_test.go b/internal/learning/analysis_test.go index aacc9a93..700e9805 100644 --- a/internal/learning/analysis_test.go +++ b/internal/learning/analysis_test.go @@ -17,7 +17,7 @@ func TestPerformStatisticalAnalysis_EmptyHistory(t *testing.T) { require.NoError(t, err) model := engine.performStatisticalAnalysis([]*memory.RoutingDecision{}) - + assert.NotNil(t, model) assert.Equal(t, 0, model.TotalRequests) assert.Empty(t, model.ModelPreferences) @@ -43,7 +43,7 @@ func TestPerformStatisticalAnalysis_ModelPreferences(t *testing.T) { {APIKeyHash: "user1", Timestamp: time.Now(), Request: memory.RequestInfo{Intent: "coding"}, Routing: memory.RoutingInfo{SelectedModel: "claude:3.5"}, Outcome: memory.OutcomeInfo{Success: true, QualityScore: 0.9}}, {APIKeyHash: "user1", Timestamp: time.Now(), Request: memory.RequestInfo{Intent: "coding"}, Routing: memory.RoutingInfo{SelectedModel: "claude:3.5"}, Outcome: memory.OutcomeInfo{Success: false, QualityScore: 0.0}}, {APIKeyHash: "user1", Timestamp: time.Now(), Request: memory.RequestInfo{Intent: "coding"}, Routing: memory.RoutingInfo{SelectedModel: "claude:3.5"}, Outcome: memory.OutcomeInfo{Success: false, QualityScore: 0.0}}, - + // GPT: 3/5 success (60%) {APIKeyHash: "user1", Timestamp: time.Now(), Request: memory.RequestInfo{Intent: "coding"}, Routing: memory.RoutingInfo{SelectedModel: "gpt:4"}, Outcome: memory.OutcomeInfo{Success: true, QualityScore: 0.7}}, {APIKeyHash: "user1", Timestamp: time.Now(), Request: memory.RequestInfo{Intent: "coding"}, Routing: memory.RoutingInfo{SelectedModel: "gpt:4"}, Outcome: memory.OutcomeInfo{Success: true, QualityScore: 0.6}}, @@ -53,11 +53,11 @@ func TestPerformStatisticalAnalysis_ModelPreferences(t *testing.T) { } model := engine.performStatisticalAnalysis(history) - + require.NotNil(t, model) assert.Equal(t, "user1", model.UserID) assert.Equal(t, 15, model.TotalRequests) - + // Should prefer Claude for coding (higher success rate and quality) codingPref, exists := model.ModelPreferences["coding"] require.True(t, exists) @@ -75,7 +75,7 @@ func TestPerformStatisticalAnalysis_ProviderBias(t *testing.T) { // Create test data with different provider performance history := []*memory.RoutingDecision{} - + // Claude provider: 9/10 success (90%) for i := 0; i < 10; i++ { success := i < 9 @@ -87,7 +87,7 @@ func TestPerformStatisticalAnalysis_ProviderBias(t *testing.T) { Outcome: memory.OutcomeInfo{Success: success, QualityScore: 0.8}, }) } - + // OpenAI provider: 5/10 success (50%) for i := 0; i < 10; i++ { success := i < 5 @@ -101,18 +101,18 @@ func TestPerformStatisticalAnalysis_ProviderBias(t *testing.T) { } model := engine.performStatisticalAnalysis(history) - + require.NotNil(t, model) - + // Global success rate should be 70% (14/20) // Claude: 90% vs 70% global = +20% = +0.4 bias (after 2x amplification) // OpenAI: 50% vs 70% global = -20% = -0.4 bias (after 2x amplification) - + claudeBias, exists := model.ProviderBias["claude"] if exists { assert.Greater(t, claudeBias, 0.0, "Claude should have positive bias") } - + openAIBias, exists := model.ProviderBias["openai"] if exists { assert.Less(t, openAIBias, 0.0, "OpenAI should have negative bias") @@ -127,7 +127,7 @@ func TestPerformStatisticalAnalysis_TimePatterns(t *testing.T) { // Create test data with time patterns history := []*memory.RoutingDecision{} - + // Morning coding (9 AM): 8 requests for i := 0; i < 8; i++ { timestamp := time.Date(2024, 1, 1, 9, 0, 0, 0, time.UTC) @@ -139,7 +139,7 @@ func TestPerformStatisticalAnalysis_TimePatterns(t *testing.T) { Outcome: memory.OutcomeInfo{Success: true, QualityScore: 0.8}, }) } - + // Afternoon writing (2 PM): 6 requests for i := 0; i < 6; i++ { timestamp := time.Date(2024, 1, 1, 14, 0, 0, 0, time.UTC) @@ -153,16 +153,16 @@ func TestPerformStatisticalAnalysis_TimePatterns(t *testing.T) { } model := engine.performStatisticalAnalysis(history) - + require.NotNil(t, model) - + timePattern, exists := model.TimePatterns["general"] require.True(t, exists) - + // Check hourly usage assert.Equal(t, 8, timePattern.HourlyUsage[9]) // 9 AM assert.Equal(t, 6, timePattern.HourlyUsage[14]) // 2 PM - + // Check peak intents (should be dominant with >50% and >=5 samples) assert.Equal(t, "coding", timePattern.PeakIntents[9]) // 8/8 = 100% coding at 9 AM assert.Equal(t, "writing", timePattern.PeakIntents[14]) // 6/6 = 100% writing at 2 PM @@ -180,16 +180,16 @@ func TestPerformStatisticalAnalysis_MinimumSamples(t *testing.T) { {APIKeyHash: "user1", Request: memory.RequestInfo{Intent: "coding"}, Routing: memory.RoutingInfo{SelectedModel: "claude:3.5"}, Outcome: memory.OutcomeInfo{Success: true}}, {APIKeyHash: "user1", Request: memory.RequestInfo{Intent: "coding"}, Routing: memory.RoutingInfo{SelectedModel: "claude:3.5"}, Outcome: memory.OutcomeInfo{Success: true}}, {APIKeyHash: "user1", Request: memory.RequestInfo{Intent: "coding"}, Routing: memory.RoutingInfo{SelectedModel: "claude:3.5"}, Outcome: memory.OutcomeInfo{Success: true}}, - + // Only 2 samples for gpt (below 5 minimum) {APIKeyHash: "user1", Request: memory.RequestInfo{Intent: "coding"}, Routing: memory.RoutingInfo{SelectedModel: "gpt:4"}, Outcome: memory.OutcomeInfo{Success: false}}, {APIKeyHash: "user1", Request: memory.RequestInfo{Intent: "coding"}, Routing: memory.RoutingInfo{SelectedModel: "gpt:4"}, Outcome: memory.OutcomeInfo{Success: false}}, } model := engine.performStatisticalAnalysis(history) - + require.NotNil(t, model) - + // Should not create preferences due to insufficient samples _, exists := model.ModelPreferences["coding"] assert.False(t, exists, "Should not create preference with insufficient samples") @@ -248,7 +248,7 @@ func BenchmarkPerformStatisticalAnalysis_LargeDataset(b *testing.B) { history := make([]*memory.RoutingDecision, 10000) intents := []string{"coding", "writing", "analysis", "general"} models := []string{"claude:3.5", "gpt:4", "gemini:pro"} - + for i := 0; i < 10000; i++ { history[i] = &memory.RoutingDecision{ APIKeyHash: "user1", @@ -270,4 +270,4 @@ func BenchmarkCalculatePreferenceConfidence(b *testing.B) { for i := 0; i < b.N; i++ { _ = CalculatePreferenceConfidence(80, 100, 0.85) } -} \ No newline at end of file +} diff --git a/internal/learning/confidence_test.go b/internal/learning/confidence_test.go index a81ab9e6..64fa68a7 100644 --- a/internal/learning/confidence_test.go +++ b/internal/learning/confidence_test.go @@ -15,7 +15,7 @@ func TestCalculatePreferenceConfidence_ZeroSamples(t *testing.T) { func TestCalculatePreferenceConfidence_PerfectSmallSample(t *testing.T) { // 10/10 success with quality 1.0 confidence := CalculatePreferenceConfidence(10, 10, 1.0) - + // Base score: (1.0 * 0.7) + (1.0 * 0.3) = 1.0 // Sample penalty: log(11)/log(101) ā‰ˆ 0.518 // Expected: 1.0 * 0.518 ā‰ˆ 0.518 @@ -26,7 +26,7 @@ func TestCalculatePreferenceConfidence_PerfectSmallSample(t *testing.T) { func TestCalculatePreferenceConfidence_PerfectLargeSample(t *testing.T) { // 100/100 success with quality 1.0 confidence := CalculatePreferenceConfidence(100, 100, 1.0) - + // Base score: (1.0 * 0.7) + (1.0 * 0.3) = 1.0 // Sample penalty: 1.0 (no penalty for 100+ samples) // Expected: 1.0 @@ -36,7 +36,7 @@ func TestCalculatePreferenceConfidence_PerfectLargeSample(t *testing.T) { func TestCalculatePreferenceConfidence_MixedResults(t *testing.T) { // 70/100 success with quality 0.8 confidence := CalculatePreferenceConfidence(70, 100, 0.8) - + // Base score: (0.7 * 0.7) + (0.8 * 0.3) = 0.49 + 0.24 = 0.73 // Sample penalty: 1.0 (no penalty for 100+ samples) // Expected: 0.73 @@ -46,7 +46,7 @@ func TestCalculatePreferenceConfidence_MixedResults(t *testing.T) { func TestCalculatePreferenceConfidence_NoQualityScore(t *testing.T) { // 80/100 success with no quality score (0.0) confidence := CalculatePreferenceConfidence(80, 100, 0.0) - + // Base score: 0.8 (success rate only when quality is 0) // Sample penalty: 1.0 (no penalty for 100+ samples) // Expected: 0.8 @@ -59,20 +59,20 @@ func TestCalculatePreferenceConfidence_SampleSizePenalty(t *testing.T) { samples int expected float64 // Approximate expected penalty factor }{ - {1, 0.15}, // log(2)/log(101) ā‰ˆ 0.15 - {5, 0.39}, // log(6)/log(101) ā‰ˆ 0.39 - {10, 0.52}, // log(11)/log(101) ā‰ˆ 0.52 - {20, 0.66}, // log(21)/log(101) ā‰ˆ 0.66 - {50, 0.85}, // log(51)/log(101) ā‰ˆ 0.85 - {100, 1.0}, // log(101)/log(101) = 1.0 - {200, 1.0}, // No penalty for 100+ + {1, 0.15}, // log(2)/log(101) ā‰ˆ 0.15 + {5, 0.39}, // log(6)/log(101) ā‰ˆ 0.39 + {10, 0.52}, // log(11)/log(101) ā‰ˆ 0.52 + {20, 0.66}, // log(21)/log(101) ā‰ˆ 0.66 + {50, 0.85}, // log(51)/log(101) ā‰ˆ 0.85 + {100, 1.0}, // log(101)/log(101) = 1.0 + {200, 1.0}, // No penalty for 100+ } for _, tc := range testCases { t.Run(fmt.Sprintf("samples_%d", tc.samples), func(t *testing.T) { // Perfect success rate to isolate sample penalty confidence := CalculatePreferenceConfidence(tc.samples, tc.samples, 1.0) - assert.InDelta(t, tc.expected, confidence, 0.05, + assert.InDelta(t, tc.expected, confidence, 0.05, "Sample size %d should have penalty factor ~%.2f", tc.samples, tc.expected) }) } @@ -95,7 +95,7 @@ func TestAdjustRuntimeConfidence_ProviderBias(t *testing.T) { adjusted := AdjustRuntimeConfidence(0.7, false, 0.5, false) // Provider bias: 0.5 * 0.1 = 0.05 assert.InDelta(t, 0.75, adjusted, 0.01) - + // Test negative bias adjusted = AdjustRuntimeConfidence(0.7, false, -0.8, false) // Provider bias: -0.8 * 0.1 = -0.08 @@ -135,43 +135,43 @@ func TestAdjustRuntimeConfidence_ClampingHigh(t *testing.T) { func TestAdjustRuntimeConfidence_WeightingFormula(t *testing.T) { // Test the exact weighting from design document testCases := []struct { - name string - base float64 + name string + base float64 hasPreference bool - providerBias float64 - isTimeMatch bool - expected float64 + providerBias float64 + isTimeMatch bool + expected float64 }{ { - name: "typical_good_case", - base: 0.75, + name: "typical_good_case", + base: 0.75, hasPreference: true, - providerBias: 0.3, - isTimeMatch: true, - expected: 0.75 + 0.15 + 0.03 + 0.1, // 1.03 -> clamped to 1.0 + providerBias: 0.3, + isTimeMatch: true, + expected: 0.75 + 0.15 + 0.03 + 0.1, // 1.03 -> clamped to 1.0 }, { - name: "typical_poor_case", - base: 0.4, + name: "typical_poor_case", + base: 0.4, hasPreference: false, - providerBias: -0.6, - isTimeMatch: false, - expected: 0.4 + 0.0 + (-0.06) + 0.0, // 0.34 + providerBias: -0.6, + isTimeMatch: false, + expected: 0.4 + 0.0 + (-0.06) + 0.0, // 0.34 }, { - name: "mixed_case", - base: 0.6, + name: "mixed_case", + base: 0.6, hasPreference: true, - providerBias: -0.2, - isTimeMatch: false, - expected: 0.6 + 0.15 + (-0.02) + 0.0, // 0.73 + providerBias: -0.2, + isTimeMatch: false, + expected: 0.6 + 0.15 + (-0.02) + 0.0, // 0.73 }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { adjusted := AdjustRuntimeConfidence(tc.base, tc.hasPreference, tc.providerBias, tc.isTimeMatch) - + // Clamp expected value to [0.0, 1.0] for comparison expected := tc.expected if expected > 1.0 { @@ -180,9 +180,9 @@ func TestAdjustRuntimeConfidence_WeightingFormula(t *testing.T) { if expected < 0.0 { expected = 0.0 } - - assert.InDelta(t, expected, adjusted, 0.01, - "Case %s: base=%.2f, pref=%t, bias=%.2f, time=%t", + + assert.InDelta(t, expected, adjusted, 0.01, + "Case %s: base=%.2f, pref=%t, bias=%.2f, time=%t", tc.name, tc.base, tc.hasPreference, tc.providerBias, tc.isTimeMatch) }) } @@ -200,4 +200,4 @@ func BenchmarkAdjustRuntimeConfidence_Minimal(b *testing.B) { for i := 0; i < b.N; i++ { _ = AdjustRuntimeConfidence(0.75, false, 0.0, false) } -} \ No newline at end of file +} diff --git a/internal/memory/analytics.go b/internal/memory/analytics.go index 9317993a..1d2aa473 100644 --- a/internal/memory/analytics.go +++ b/internal/memory/analytics.go @@ -15,27 +15,27 @@ import ( // It provides provider stats, model performance metrics, and trend analysis. type AnalyticsEngine struct { analyticsDir string - + // Cached data providerStats map[string]*ProviderStats modelPerformance map[string]*ModelPerformance - + // Synchronization mu sync.RWMutex - + // Last update tracking lastUpdate time.Time } // AnalyticsSummary provides a comprehensive view of system analytics. type AnalyticsSummary struct { - GeneratedAt time.Time `json:"generated_at"` - TimeRange TimeRange `json:"time_range"` - ProviderStats map[string]*ProviderStats `json:"provider_stats"` - ModelPerformance map[string]*ModelPerformance `json:"model_performance"` - TierEffectiveness *TierEffectiveness `json:"tier_effectiveness"` - CostAnalysis *CostAnalysis `json:"cost_analysis"` - TrendAnalysis *TrendAnalysis `json:"trend_analysis"` + GeneratedAt time.Time `json:"generated_at"` + TimeRange TimeRange `json:"time_range"` + ProviderStats map[string]*ProviderStats `json:"provider_stats"` + ModelPerformance map[string]*ModelPerformance `json:"model_performance"` + TierEffectiveness *TierEffectiveness `json:"tier_effectiveness"` + CostAnalysis *CostAnalysis `json:"cost_analysis"` + TrendAnalysis *TrendAnalysis `json:"trend_analysis"` } // TimeRange represents a time period for analytics. @@ -79,11 +79,11 @@ type DailyCost struct { // TrendAnalysis provides trend information over time. type TrendAnalysis struct { - RequestVolumeTrend []DailyVolume `json:"request_volume_trend"` - SuccessRateTrend []DailyMetric `json:"success_rate_trend"` - LatencyTrend []DailyMetric `json:"latency_trend"` + RequestVolumeTrend []DailyVolume `json:"request_volume_trend"` + SuccessRateTrend []DailyMetric `json:"success_rate_trend"` + LatencyTrend []DailyMetric `json:"latency_trend"` PopularModels []ModelPopularity `json:"popular_models"` - PeakHours []HourlyStats `json:"peak_hours"` + PeakHours []HourlyStats `json:"peak_hours"` } // DailyVolume represents request volume for a specific day. @@ -108,8 +108,8 @@ type ModelPopularity struct { // HourlyStats represents statistics for a specific hour of day. type HourlyStats struct { - Hour int `json:"hour"` - Requests int `json:"requests"` + Hour int `json:"hour"` + Requests int `json:"requests"` AvgLatency float64 `json:"avg_latency"` } @@ -126,29 +126,29 @@ func NewAnalyticsEngine(analyticsDir string) *AnalyticsEngine { func (ae *AnalyticsEngine) ComputeAnalytics(decisions []*RoutingDecision) (*AnalyticsSummary, error) { ae.mu.Lock() defer ae.mu.Unlock() - + if len(decisions) == 0 { return ae.createEmptyAnalytics(), nil } - + // Determine time range timeRange := ae.calculateTimeRange(decisions) - + // Compute provider statistics providerStats := ae.computeProviderStats(decisions) - + // Compute model performance modelPerformance := ae.computeModelPerformance(decisions) - + // Compute tier effectiveness tierEffectiveness := ae.computeTierEffectiveness(decisions) - + // Compute cost analysis costAnalysis := ae.computeCostAnalysis(decisions) - + // Compute trend analysis trendAnalysis := ae.computeTrendAnalysis(decisions) - + // Create summary summary := &AnalyticsSummary{ GeneratedAt: time.Now(), @@ -159,17 +159,17 @@ func (ae *AnalyticsEngine) ComputeAnalytics(decisions []*RoutingDecision) (*Anal CostAnalysis: costAnalysis, TrendAnalysis: trendAnalysis, } - + // Cache results ae.providerStats = providerStats ae.modelPerformance = modelPerformance ae.lastUpdate = time.Now() - + // Store results to disk if err := ae.storeAnalytics(summary); err != nil { return nil, fmt.Errorf("failed to store analytics: %w", err) } - + return summary, nil } @@ -183,11 +183,11 @@ func (ae *AnalyticsEngine) calculateTimeRange(decisions []*RoutingDecision) Time Days: 0, } } - + // Find earliest and latest timestamps earliest := decisions[0].Timestamp latest := decisions[0].Timestamp - + for _, decision := range decisions { if decision.Timestamp.Before(earliest) { earliest = decision.Timestamp @@ -196,9 +196,9 @@ func (ae *AnalyticsEngine) calculateTimeRange(decisions []*RoutingDecision) Time latest = decision.Timestamp } } - + days := int(latest.Sub(earliest).Hours()/24) + 1 - + return TimeRange{ StartTime: earliest, EndTime: latest, @@ -209,26 +209,26 @@ func (ae *AnalyticsEngine) calculateTimeRange(decisions []*RoutingDecision) Time // computeProviderStats computes statistics for each provider. func (ae *AnalyticsEngine) computeProviderStats(decisions []*RoutingDecision) map[string]*ProviderStats { providerData := make(map[string]*providerStatsAccumulator) - + // Accumulate data for each provider for _, decision := range decisions { provider := ae.extractProvider(decision.Routing.SelectedModel) - + if _, exists := providerData[provider]; !exists { providerData[provider] = &providerStatsAccumulator{} } - + acc := providerData[provider] acc.totalRequests++ acc.totalLatency += float64(decision.Outcome.ResponseTimeMs) - + if decision.Outcome.Success { acc.successfulRequests++ } else { acc.failedRequests++ } } - + // Convert to final stats stats := make(map[string]*ProviderStats) for provider, acc := range providerData { @@ -236,17 +236,17 @@ func (ae *AnalyticsEngine) computeProviderStats(decisions []*RoutingDecision) ma if acc.totalRequests > 0 { successRate = float64(acc.successfulRequests) / float64(acc.totalRequests) } - + avgLatency := 0.0 if acc.totalRequests > 0 { avgLatency = acc.totalLatency / float64(acc.totalRequests) } - + errorRate := 0.0 if acc.totalRequests > 0 { errorRate = float64(acc.failedRequests) / float64(acc.totalRequests) } - + stats[provider] = &ProviderStats{ Provider: provider, TotalRequests: acc.totalRequests, @@ -256,34 +256,34 @@ func (ae *AnalyticsEngine) computeProviderStats(decisions []*RoutingDecision) ma LastUpdated: time.Now(), } } - + return stats } // computeModelPerformance computes performance metrics for each model. func (ae *AnalyticsEngine) computeModelPerformance(decisions []*RoutingDecision) map[string]*ModelPerformance { modelData := make(map[string]*modelPerformanceAccumulator) - + // Accumulate data for each model for _, decision := range decisions { model := decision.Routing.SelectedModel - + if _, exists := modelData[model]; !exists { modelData[model] = &modelPerformanceAccumulator{} } - + acc := modelData[model] acc.totalRequests++ acc.totalQuality += decision.Outcome.QualityScore - + if decision.Outcome.Success { acc.successfulRequests++ } - + // Estimate cost (simplified - in real implementation, this would use actual pricing) acc.totalCost += ae.estimateRequestCost(model, decision) } - + // Convert to final performance metrics performance := make(map[string]*ModelPerformance) for model, acc := range modelData { @@ -291,17 +291,17 @@ func (ae *AnalyticsEngine) computeModelPerformance(decisions []*RoutingDecision) if acc.totalRequests > 0 { successRate = float64(acc.successfulRequests) / float64(acc.totalRequests) } - + avgQuality := 0.0 if acc.totalRequests > 0 { avgQuality = acc.totalQuality / float64(acc.totalRequests) } - + avgCost := 0.0 if acc.totalRequests > 0 { avgCost = acc.totalCost / float64(acc.totalRequests) } - + performance[model] = &ModelPerformance{ Model: model, TotalRequests: acc.totalRequests, @@ -310,35 +310,35 @@ func (ae *AnalyticsEngine) computeModelPerformance(decisions []*RoutingDecision) AvgCostPerReq: avgCost, } } - + return performance } // computeTierEffectiveness computes effectiveness metrics for each routing tier. func (ae *AnalyticsEngine) computeTierEffectiveness(decisions []*RoutingDecision) *TierEffectiveness { tierData := make(map[string]*tierStatsAccumulator) - + // Initialize accumulators for all tiers tiers := []string{"reflex", "semantic", "cognitive", "learned"} for _, tier := range tiers { tierData[tier] = &tierStatsAccumulator{} } - + // Accumulate data for each tier for _, decision := range decisions { tier := decision.Routing.Tier - + if acc, exists := tierData[tier]; exists { acc.totalRequests++ acc.totalLatency += float64(decision.Routing.LatencyMs) acc.totalConfidence += decision.Routing.Confidence - + if decision.Outcome.Success { acc.successfulRequests++ } } } - + // Convert to final tier stats return &TierEffectiveness{ ReflexTier: ae.convertTierStats(tierData["reflex"]), @@ -355,34 +355,34 @@ func (ae *AnalyticsEngine) computeCostAnalysis(decisions []*RoutingDecision) *Co costByModel := make(map[string]float64) dailyCosts := make(map[string]float64) var localRequests int - + for _, decision := range decisions { cost := ae.estimateRequestCost(decision.Routing.SelectedModel, decision) totalCost += cost - + // Cost by provider provider := ae.extractProvider(decision.Routing.SelectedModel) costByProvider[provider] += cost - + // Cost by model costByModel[decision.Routing.SelectedModel] += cost - + // Daily costs date := decision.Timestamp.Format("2006-01-02") dailyCosts[date] += cost - + // Count local requests for savings calculation if ae.isLocalProvider(provider) { localRequests++ } } - + // Calculate average cost per request avgCostPerReq := 0.0 if len(decisions) > 0 { avgCostPerReq = totalCost / float64(len(decisions)) } - + // Convert daily costs to sorted slice var costTrend []DailyCost for date, cost := range dailyCosts { @@ -394,10 +394,10 @@ func (ae *AnalyticsEngine) computeCostAnalysis(decisions []*RoutingDecision) *Co sort.Slice(costTrend, func(i, j int) bool { return costTrend[i].Date < costTrend[j].Date }) - + // Estimate savings from local models (assume cloud cost would be $0.01 per request) savingsFromLocal := float64(localRequests) * 0.01 - + return &CostAnalysis{ TotalCost: totalCost, CostByProvider: costByProvider, @@ -416,19 +416,19 @@ func (ae *AnalyticsEngine) computeTrendAnalysis(decisions []*RoutingDecision) *T dailyLatency := make(map[string]*dailyLatencyAccumulator) modelCounts := make(map[string]int) hourlyCounts := make(map[int]*hourlyStatsAccumulator) - + // Initialize hourly accumulators for i := 0; i < 24; i++ { hourlyCounts[i] = &hourlyStatsAccumulator{} } - + for _, decision := range decisions { date := decision.Timestamp.Format("2006-01-02") hour := decision.Timestamp.Hour() - + // Daily volume dailyVolume[date]++ - + // Daily success rate if _, exists := dailySuccess[date]; !exists { dailySuccess[date] = &dailySuccessAccumulator{} @@ -437,22 +437,22 @@ func (ae *AnalyticsEngine) computeTrendAnalysis(decisions []*RoutingDecision) *T if decision.Outcome.Success { dailySuccess[date].successful++ } - + // Daily latency if _, exists := dailyLatency[date]; !exists { dailyLatency[date] = &dailyLatencyAccumulator{} } dailyLatency[date].total++ dailyLatency[date].totalLatency += float64(decision.Outcome.ResponseTimeMs) - + // Model popularity modelCounts[decision.Routing.SelectedModel]++ - + // Hourly stats hourlyCounts[hour].requests++ hourlyCounts[hour].totalLatency += float64(decision.Outcome.ResponseTimeMs) } - + // Convert to trend data var volumeTrend []DailyVolume for date, count := range dailyVolume { @@ -464,7 +464,7 @@ func (ae *AnalyticsEngine) computeTrendAnalysis(decisions []*RoutingDecision) *T sort.Slice(volumeTrend, func(i, j int) bool { return volumeTrend[i].Date < volumeTrend[j].Date }) - + var successTrend []DailyMetric for date, acc := range dailySuccess { rate := 0.0 @@ -479,7 +479,7 @@ func (ae *AnalyticsEngine) computeTrendAnalysis(decisions []*RoutingDecision) *T sort.Slice(successTrend, func(i, j int) bool { return successTrend[i].Date < successTrend[j].Date }) - + var latencyTrend []DailyMetric for date, acc := range dailyLatency { avgLatency := 0.0 @@ -494,7 +494,7 @@ func (ae *AnalyticsEngine) computeTrendAnalysis(decisions []*RoutingDecision) *T sort.Slice(latencyTrend, func(i, j int) bool { return latencyTrend[i].Date < latencyTrend[j].Date }) - + // Popular models var popularModels []ModelPopularity totalRequests := len(decisions) @@ -503,13 +503,13 @@ func (ae *AnalyticsEngine) computeTrendAnalysis(decisions []*RoutingDecision) *T if totalRequests > 0 { percentage = float64(count) / float64(totalRequests) * 100 } - + // Calculate success rate for this model successRate := 0.0 if perf, exists := ae.modelPerformance[model]; exists { successRate = perf.SuccessRate } - + popularModels = append(popularModels, ModelPopularity{ Model: model, Requests: count, @@ -520,7 +520,7 @@ func (ae *AnalyticsEngine) computeTrendAnalysis(decisions []*RoutingDecision) *T sort.Slice(popularModels, func(i, j int) bool { return popularModels[i].Requests > popularModels[j].Requests }) - + // Peak hours var peakHours []HourlyStats for hour, acc := range hourlyCounts { @@ -528,7 +528,7 @@ func (ae *AnalyticsEngine) computeTrendAnalysis(decisions []*RoutingDecision) *T if acc.requests > 0 { avgLatency = acc.totalLatency / float64(acc.requests) } - + peakHours = append(peakHours, HourlyStats{ Hour: hour, Requests: acc.requests, @@ -538,7 +538,7 @@ func (ae *AnalyticsEngine) computeTrendAnalysis(decisions []*RoutingDecision) *T sort.Slice(peakHours, func(i, j int) bool { return peakHours[i].Requests > peakHours[j].Requests }) - + return &TrendAnalysis{ RequestVolumeTrend: volumeTrend, SuccessRateTrend: successTrend, @@ -592,17 +592,17 @@ func (ae *AnalyticsEngine) convertTierStats(acc *tierStatsAccumulator) *TierStat if acc.totalRequests > 0 { successRate = float64(acc.successfulRequests) / float64(acc.totalRequests) } - + avgLatency := 0.0 if acc.totalRequests > 0 { avgLatency = acc.totalLatency / float64(acc.totalRequests) } - + avgConfidence := 0.0 if acc.totalRequests > 0 { avgConfidence = acc.totalConfidence / float64(acc.totalRequests) } - + return &TierStats{ TotalRequests: acc.totalRequests, SuccessRate: successRate, @@ -634,15 +634,15 @@ func (ae *AnalyticsEngine) isLocalProvider(provider string) bool { // estimateRequestCost estimates the cost of a request (simplified). func (ae *AnalyticsEngine) estimateRequestCost(model string, decision *RoutingDecision) float64 { provider := ae.extractProvider(model) - + // Local providers have no cost if ae.isLocalProvider(provider) { return 0.0 } - + // Simplified cost estimation based on provider and content length baseCost := 0.001 // $0.001 base cost - + switch provider { case "claudecli", "claude": return baseCost * 2.0 // Claude is more expensive @@ -680,19 +680,19 @@ func (ae *AnalyticsEngine) storeAnalytics(summary *AnalyticsSummary) error { if err := ae.writeJSONFile(providerStatsPath, summary.ProviderStats); err != nil { return fmt.Errorf("failed to store provider stats: %w", err) } - + // Store model performance modelPerfPath := filepath.Join(ae.analyticsDir, "model-performance.json") if err := ae.writeJSONFile(modelPerfPath, summary.ModelPerformance); err != nil { return fmt.Errorf("failed to store model performance: %w", err) } - + // Store complete summary summaryPath := filepath.Join(ae.analyticsDir, "analytics-summary.json") if err := ae.writeJSONFile(summaryPath, summary); err != nil { return fmt.Errorf("failed to store analytics summary: %w", err) } - + return nil } @@ -703,14 +703,14 @@ func (ae *AnalyticsEngine) writeJSONFile(path string, data interface{}) error { return fmt.Errorf("failed to open file %s: %w", path, err) } defer file.Close() - + encoder := json.NewEncoder(file) encoder.SetIndent("", " ") - + if err := encoder.Encode(data); err != nil { return fmt.Errorf("failed to encode JSON: %w", err) } - + return nil } @@ -718,9 +718,9 @@ func (ae *AnalyticsEngine) writeJSONFile(path string, data interface{}) error { func (ae *AnalyticsEngine) LoadAnalytics() (*AnalyticsSummary, error) { ae.mu.RLock() defer ae.mu.RUnlock() - + summaryPath := filepath.Join(ae.analyticsDir, "analytics-summary.json") - + file, err := os.Open(summaryPath) if err != nil { if os.IsNotExist(err) { @@ -729,14 +729,14 @@ func (ae *AnalyticsEngine) LoadAnalytics() (*AnalyticsSummary, error) { return nil, fmt.Errorf("failed to open analytics summary: %w", err) } defer file.Close() - + var summary AnalyticsSummary decoder := json.NewDecoder(file) - + if err := decoder.Decode(&summary); err != nil { return nil, fmt.Errorf("failed to decode analytics summary: %w", err) } - + return &summary, nil } @@ -744,13 +744,13 @@ func (ae *AnalyticsEngine) LoadAnalytics() (*AnalyticsSummary, error) { func (ae *AnalyticsEngine) GetProviderStats() map[string]*ProviderStats { ae.mu.RLock() defer ae.mu.RUnlock() - + // Return copy to prevent external modification stats := make(map[string]*ProviderStats) for k, v := range ae.providerStats { stats[k] = v } - + return stats } @@ -758,13 +758,13 @@ func (ae *AnalyticsEngine) GetProviderStats() map[string]*ProviderStats { func (ae *AnalyticsEngine) GetModelPerformance() map[string]*ModelPerformance { ae.mu.RLock() defer ae.mu.RUnlock() - + // Return copy to prevent external modification performance := make(map[string]*ModelPerformance) for k, v := range ae.modelPerformance { performance[k] = v } - + return performance } @@ -772,6 +772,6 @@ func (ae *AnalyticsEngine) GetModelPerformance() map[string]*ModelPerformance { func (ae *AnalyticsEngine) GetLastUpdate() time.Time { ae.mu.RLock() defer ae.mu.RUnlock() - + return ae.lastUpdate -} \ No newline at end of file +} diff --git a/internal/memory/analytics_test.go b/internal/memory/analytics_test.go index 0cb9118e..5b591a43 100644 --- a/internal/memory/analytics_test.go +++ b/internal/memory/analytics_test.go @@ -9,28 +9,28 @@ import ( func TestAnalyticsEngine_ComputeAnalytics_Empty(t *testing.T) { tempDir := t.TempDir() - + engine := NewAnalyticsEngine(tempDir) - + // Compute analytics with no decisions summary, err := engine.ComputeAnalytics([]*RoutingDecision{}) if err != nil { t.Fatalf("Failed to compute analytics: %v", err) } - + // Verify empty summary if summary.GeneratedAt.IsZero() { t.Error("Expected non-zero generated timestamp") } - + if summary.TimeRange.Days != 0 { t.Errorf("Expected 0 days, got %d", summary.TimeRange.Days) } - + if len(summary.ProviderStats) != 0 { t.Errorf("Expected 0 provider stats, got %d", len(summary.ProviderStats)) } - + if len(summary.ModelPerformance) != 0 { t.Errorf("Expected 0 model performance entries, got %d", len(summary.ModelPerformance)) } @@ -38,9 +38,9 @@ func TestAnalyticsEngine_ComputeAnalytics_Empty(t *testing.T) { func TestAnalyticsEngine_ComputeAnalytics_WithData(t *testing.T) { tempDir := t.TempDir() - + engine := NewAnalyticsEngine(tempDir) - + // Create test routing decisions baseTime := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC) decisions := []*RoutingDecision{ @@ -106,23 +106,23 @@ func TestAnalyticsEngine_ComputeAnalytics_WithData(t *testing.T) { }, }, } - + // Compute analytics summary, err := engine.ComputeAnalytics(decisions) if err != nil { t.Fatalf("Failed to compute analytics: %v", err) } - + // Verify time range if summary.TimeRange.Days != 1 { t.Errorf("Expected 1 day, got %d", summary.TimeRange.Days) } - + // Verify provider stats if len(summary.ProviderStats) != 3 { t.Errorf("Expected 3 providers, got %d", len(summary.ProviderStats)) } - + // Check Claude stats claudeStats, exists := summary.ProviderStats["claudecli"] if !exists { @@ -134,7 +134,7 @@ func TestAnalyticsEngine_ComputeAnalytics_WithData(t *testing.T) { if claudeStats.SuccessRate != 1.0 { t.Errorf("Expected 100%% Claude success rate, got %.2f", claudeStats.SuccessRate) } - + // Check Gemini stats geminiStats, exists := summary.ProviderStats["geminicli"] if !exists { @@ -146,7 +146,7 @@ func TestAnalyticsEngine_ComputeAnalytics_WithData(t *testing.T) { if geminiStats.SuccessRate != 0.0 { t.Errorf("Expected 0%% Gemini success rate, got %.2f", geminiStats.SuccessRate) } - + // Check Ollama stats ollamaStats, exists := summary.ProviderStats["ollama"] if !exists { @@ -158,12 +158,12 @@ func TestAnalyticsEngine_ComputeAnalytics_WithData(t *testing.T) { if ollamaStats.SuccessRate != 1.0 { t.Errorf("Expected 100%% Ollama success rate, got %.2f", ollamaStats.SuccessRate) } - + // Verify model performance if len(summary.ModelPerformance) != 3 { t.Errorf("Expected 3 models, got %d", len(summary.ModelPerformance)) } - + claudeModel, exists := summary.ModelPerformance["claudecli:claude-sonnet-4"] if !exists { t.Fatal("Expected Claude model performance") @@ -174,51 +174,51 @@ func TestAnalyticsEngine_ComputeAnalytics_WithData(t *testing.T) { if claudeModel.AvgQualityScore != 0.85 { t.Errorf("Expected 0.85 Claude quality score, got %.2f", claudeModel.AvgQualityScore) } - + // Verify tier effectiveness if summary.TierEffectiveness == nil { t.Fatal("Expected tier effectiveness data") } - + if summary.TierEffectiveness.SemanticTier.TotalRequests != 2 { t.Errorf("Expected 2 semantic tier requests, got %d", summary.TierEffectiveness.SemanticTier.TotalRequests) } - + if summary.TierEffectiveness.CognitiveTier.TotalRequests != 1 { t.Errorf("Expected 1 cognitive tier request, got %d", summary.TierEffectiveness.CognitiveTier.TotalRequests) } - + // Verify cost analysis if summary.CostAnalysis == nil { t.Fatal("Expected cost analysis data") } - + if summary.CostAnalysis.TotalCost <= 0 { t.Error("Expected positive total cost") } - + if len(summary.CostAnalysis.CostByProvider) != 3 { t.Errorf("Expected 3 providers in cost analysis, got %d", len(summary.CostAnalysis.CostByProvider)) } - + // Ollama should have 0 cost (local provider) if summary.CostAnalysis.CostByProvider["ollama"] != 0.0 { t.Errorf("Expected 0 cost for Ollama, got %.4f", summary.CostAnalysis.CostByProvider["ollama"]) } - + // Verify trend analysis if summary.TrendAnalysis == nil { t.Fatal("Expected trend analysis data") } - + if len(summary.TrendAnalysis.RequestVolumeTrend) != 1 { t.Errorf("Expected 1 day in volume trend, got %d", len(summary.TrendAnalysis.RequestVolumeTrend)) } - + if summary.TrendAnalysis.RequestVolumeTrend[0].Requests != 3 { t.Errorf("Expected 3 requests in volume trend, got %d", summary.TrendAnalysis.RequestVolumeTrend[0].Requests) } - + if len(summary.TrendAnalysis.PopularModels) != 3 { t.Errorf("Expected 3 popular models, got %d", len(summary.TrendAnalysis.PopularModels)) } @@ -226,7 +226,7 @@ func TestAnalyticsEngine_ComputeAnalytics_WithData(t *testing.T) { func TestAnalyticsEngine_ProviderExtraction(t *testing.T) { engine := NewAnalyticsEngine("") - + testCases := []struct { model string expected string @@ -237,7 +237,7 @@ func TestAnalyticsEngine_ProviderExtraction(t *testing.T) { {"openai:gpt-4", "openai"}, {"simple-model", "simple-model"}, } - + for _, tc := range testCases { result := engine.extractProvider(tc.model) if result != tc.expected { @@ -248,7 +248,7 @@ func TestAnalyticsEngine_ProviderExtraction(t *testing.T) { func TestAnalyticsEngine_LocalProviderDetection(t *testing.T) { engine := NewAnalyticsEngine("") - + testCases := []struct { provider string expected bool @@ -260,7 +260,7 @@ func TestAnalyticsEngine_LocalProviderDetection(t *testing.T) { {"geminicli", false}, {"openai", false}, } - + for _, tc := range testCases { result := engine.isLocalProvider(tc.provider) if result != tc.expected { @@ -271,28 +271,28 @@ func TestAnalyticsEngine_LocalProviderDetection(t *testing.T) { func TestAnalyticsEngine_CostEstimation(t *testing.T) { engine := NewAnalyticsEngine("") - + decision := &RoutingDecision{ Request: RequestInfo{ ContentLength: 100, }, } - + testCases := []struct { model string expectedCost float64 }{ - {"ollama:codellama", 0.0}, // Local provider - {"lmstudio:model", 0.0}, // Local provider - {"claudecli:claude", 0.002}, // 2x base cost - {"geminicli:gemini", 0.0015}, // 1.5x base cost - {"openai:gpt-4", 0.0018}, // 1.8x base cost - {"unknown:model", 0.001}, // Base cost - } - + {"ollama:codellama", 0.0}, // Local provider + {"lmstudio:model", 0.0}, // Local provider + {"claudecli:claude", 0.002}, // 2x base cost + {"geminicli:gemini", 0.0015}, // 1.5x base cost + {"openai:gpt-4", 0.0018}, // 1.8x base cost + {"unknown:model", 0.001}, // Base cost + } + for _, tc := range testCases { result := engine.estimateRequestCost(tc.model, decision) - if abs(result - tc.expectedCost) > 0.0001 { + if abs(result-tc.expectedCost) > 0.0001 { t.Errorf("estimateRequestCost(%s): expected %.4f, got %.4f", tc.model, tc.expectedCost, result) } } @@ -308,9 +308,9 @@ func abs(x float64) float64 { func TestAnalyticsEngine_StoreAndLoadAnalytics(t *testing.T) { tempDir := t.TempDir() - + engine := NewAnalyticsEngine(tempDir) - + // Create test data decisions := []*RoutingDecision{ { @@ -331,53 +331,53 @@ func TestAnalyticsEngine_StoreAndLoadAnalytics(t *testing.T) { }, }, } - + // Compute and store analytics originalSummary, err := engine.ComputeAnalytics(decisions) if err != nil { t.Fatalf("Failed to compute analytics: %v", err) } - + // Verify files were created expectedFiles := []string{ "provider-stats.json", "model-performance.json", "analytics-summary.json", } - + for _, fileName := range expectedFiles { filePath := filepath.Join(tempDir, fileName) if _, err := os.Stat(filePath); os.IsNotExist(err) { t.Errorf("Expected file %s was not created", fileName) } } - + // Load analytics back loadedSummary, err := engine.LoadAnalytics() if err != nil { t.Fatalf("Failed to load analytics: %v", err) } - + // Verify loaded data matches original if len(loadedSummary.ProviderStats) != len(originalSummary.ProviderStats) { t.Errorf("Provider stats count mismatch: expected %d, got %d", len(originalSummary.ProviderStats), len(loadedSummary.ProviderStats)) } - + if len(loadedSummary.ModelPerformance) != len(originalSummary.ModelPerformance) { t.Errorf("Model performance count mismatch: expected %d, got %d", len(originalSummary.ModelPerformance), len(loadedSummary.ModelPerformance)) } - + // Verify specific provider stats originalClaude := originalSummary.ProviderStats["claudecli"] loadedClaude := loadedSummary.ProviderStats["claudecli"] - + if originalClaude.TotalRequests != loadedClaude.TotalRequests { t.Errorf("Claude total requests mismatch: expected %d, got %d", originalClaude.TotalRequests, loadedClaude.TotalRequests) } - + if originalClaude.SuccessRate != loadedClaude.SuccessRate { t.Errorf("Claude success rate mismatch: expected %.2f, got %.2f", originalClaude.SuccessRate, loadedClaude.SuccessRate) @@ -386,20 +386,20 @@ func TestAnalyticsEngine_StoreAndLoadAnalytics(t *testing.T) { func TestAnalyticsEngine_LoadNonexistentAnalytics(t *testing.T) { tempDir := t.TempDir() - + engine := NewAnalyticsEngine(tempDir) - + // Load analytics when no file exists summary, err := engine.LoadAnalytics() if err != nil { t.Fatalf("Failed to load analytics: %v", err) } - + // Should return empty analytics if len(summary.ProviderStats) != 0 { t.Errorf("Expected 0 provider stats, got %d", len(summary.ProviderStats)) } - + if len(summary.ModelPerformance) != 0 { t.Errorf("Expected 0 model performance entries, got %d", len(summary.ModelPerformance)) } @@ -407,13 +407,13 @@ func TestAnalyticsEngine_LoadNonexistentAnalytics(t *testing.T) { func TestAnalyticsEngine_TrendAnalysis(t *testing.T) { tempDir := t.TempDir() - + engine := NewAnalyticsEngine(tempDir) - + // Create decisions across multiple days and hours baseTime := time.Date(2023, 1, 1, 9, 0, 0, 0, time.UTC) var decisions []*RoutingDecision - + // Day 1: 5 requests for i := 0; i < 5; i++ { decisions = append(decisions, &RoutingDecision{ @@ -435,7 +435,7 @@ func TestAnalyticsEngine_TrendAnalysis(t *testing.T) { }, }) } - + // Day 2: 3 requests day2 := baseTime.AddDate(0, 0, 1) for i := 0; i < 3; i++ { @@ -458,19 +458,19 @@ func TestAnalyticsEngine_TrendAnalysis(t *testing.T) { }, }) } - + // Compute analytics summary, err := engine.ComputeAnalytics(decisions) if err != nil { t.Fatalf("Failed to compute analytics: %v", err) } - + // Verify request volume trend volumeTrend := summary.TrendAnalysis.RequestVolumeTrend if len(volumeTrend) != 2 { t.Fatalf("Expected 2 days in volume trend, got %d", len(volumeTrend)) } - + // Should be sorted by date if volumeTrend[0].Date != "2023-01-01" { t.Errorf("Expected first date to be 2023-01-01, got %s", volumeTrend[0].Date) @@ -478,33 +478,33 @@ func TestAnalyticsEngine_TrendAnalysis(t *testing.T) { if volumeTrend[0].Requests != 5 { t.Errorf("Expected 5 requests on first day, got %d", volumeTrend[0].Requests) } - + if volumeTrend[1].Date != "2023-01-02" { t.Errorf("Expected second date to be 2023-01-02, got %s", volumeTrend[1].Date) } if volumeTrend[1].Requests != 3 { t.Errorf("Expected 3 requests on second day, got %d", volumeTrend[1].Requests) } - + // Verify success rate trend successTrend := summary.TrendAnalysis.SuccessRateTrend if len(successTrend) != 2 { t.Fatalf("Expected 2 days in success trend, got %d", len(successTrend)) } - + // All requests were successful for i, trend := range successTrend { if trend.Value != 1.0 { t.Errorf("Day %d: expected 100%% success rate, got %.2f", i, trend.Value) } } - + // Verify popular models popularModels := summary.TrendAnalysis.PopularModels if len(popularModels) != 2 { t.Fatalf("Expected 2 popular models, got %d", len(popularModels)) } - + // Should be sorted by request count (descending) if popularModels[0].Model != "claudecli:claude-sonnet-4" { t.Errorf("Expected most popular model to be Claude, got %s", popularModels[0].Model) @@ -515,13 +515,13 @@ func TestAnalyticsEngine_TrendAnalysis(t *testing.T) { if popularModels[0].Percentage != 62.5 { // 5/8 * 100 t.Errorf("Expected Claude percentage to be 62.5, got %.1f", popularModels[0].Percentage) } - + // Verify peak hours peakHours := summary.TrendAnalysis.PeakHours if len(peakHours) != 24 { t.Fatalf("Expected 24 hours in peak hours, got %d", len(peakHours)) } - + // Should be sorted by request count (descending) // Hours 9, 10, 11 should have the most requests (2 each) maxRequests := peakHours[0].Requests @@ -532,9 +532,9 @@ func TestAnalyticsEngine_TrendAnalysis(t *testing.T) { func TestAnalyticsEngine_CachedData(t *testing.T) { tempDir := t.TempDir() - + engine := NewAnalyticsEngine(tempDir) - + // Create test data decisions := []*RoutingDecision{ { @@ -555,19 +555,19 @@ func TestAnalyticsEngine_CachedData(t *testing.T) { }, }, } - + // Compute analytics _, err := engine.ComputeAnalytics(decisions) if err != nil { t.Fatalf("Failed to compute analytics: %v", err) } - + // Get cached provider stats providerStats := engine.GetProviderStats() if len(providerStats) != 1 { t.Errorf("Expected 1 cached provider stat, got %d", len(providerStats)) } - + claudeStats, exists := providerStats["claudecli"] if !exists { t.Fatal("Expected cached Claude stats") @@ -575,13 +575,13 @@ func TestAnalyticsEngine_CachedData(t *testing.T) { if claudeStats.TotalRequests != 1 { t.Errorf("Expected 1 cached request, got %d", claudeStats.TotalRequests) } - + // Get cached model performance modelPerformance := engine.GetModelPerformance() if len(modelPerformance) != 1 { t.Errorf("Expected 1 cached model performance, got %d", len(modelPerformance)) } - + claudeModel, exists := modelPerformance["claudecli:claude-sonnet-4"] if !exists { t.Fatal("Expected cached Claude model performance") @@ -589,10 +589,10 @@ func TestAnalyticsEngine_CachedData(t *testing.T) { if claudeModel.TotalRequests != 1 { t.Errorf("Expected 1 cached model request, got %d", claudeModel.TotalRequests) } - + // Verify last update time lastUpdate := engine.GetLastUpdate() if lastUpdate.IsZero() { t.Error("Expected non-zero last update time") } -} \ No newline at end of file +} diff --git a/internal/memory/benchmark_test.go b/internal/memory/benchmark_test.go index 07885b84..9b9d041a 100644 --- a/internal/memory/benchmark_test.go +++ b/internal/memory/benchmark_test.go @@ -85,4 +85,4 @@ func BenchmarkGetHistory(b *testing.B) { b.Fatalf("Failed to get history: %v", err) } } -} \ No newline at end of file +} diff --git a/internal/memory/preferences_test.go b/internal/memory/preferences_test.go index 0ad902df..d9602c94 100644 --- a/internal/memory/preferences_test.go +++ b/internal/memory/preferences_test.go @@ -693,10 +693,10 @@ func TestConcurrentAccess(t *testing.T) { defer store.Close() apiKeyHash := "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - + // Run concurrent operations done := make(chan bool, 10) - + // Concurrent reads for i := 0; i < 5; i++ { go func() { @@ -709,7 +709,7 @@ func TestConcurrentAccess(t *testing.T) { } }() } - + // Concurrent writes for i := 0; i < 5; i++ { go func(id int) { @@ -726,9 +726,9 @@ func TestConcurrentAccess(t *testing.T) { } }(i) } - + // Wait for all goroutines to complete for i := 0; i < 10; i++ { <-done } -} \ No newline at end of file +} diff --git a/internal/memory/property_test.go b/internal/memory/property_test.go index b7bb3282..69594027 100644 --- a/internal/memory/property_test.go +++ b/internal/memory/property_test.go @@ -415,12 +415,12 @@ func genOutcomeInfo() gopter.Gen { ).Map(func(values []interface{}) OutcomeInfo { success := values[0].(bool) errorMsg := values[2].(string) - + // If success is true, clear error message if success { errorMsg = "" } - + return OutcomeInfo{ Success: success, ResponseTimeMs: values[1].(int64), @@ -468,4 +468,4 @@ func genQuirk() gopter.Gen { Severity: values[4].(string), } }) -} \ No newline at end of file +} diff --git a/internal/memory/routing_history.go b/internal/memory/routing_history.go index 4405c32e..73a9bfae 100644 --- a/internal/memory/routing_history.go +++ b/internal/memory/routing_history.go @@ -139,7 +139,7 @@ func (rhs *RoutingHistoryStore) GetHistory(apiKeyHash string, limit int) ([]*Rou // Read all matching decisions var decisions []*RoutingDecision scanner := bufio.NewScanner(file) - + // Increase buffer size for large lines buf := make([]byte, 0, 64*1024) scanner.Buffer(buf, 1024*1024) @@ -202,7 +202,7 @@ func (rhs *RoutingHistoryStore) GetAllHistory(limit int) ([]*RoutingDecision, er // Read all decisions var decisions []*RoutingDecision scanner := bufio.NewScanner(file) - + // Increase buffer size for large lines buf := make([]byte, 0, 64*1024) scanner.Buffer(buf, 1024*1024) @@ -248,7 +248,7 @@ func (rhs *RoutingHistoryStore) writeWorker() { case op := <-rhs.writeQueue: // Process write operation err := rhs.writeDecision(op.decision) - + // Send result back to caller select { case op.errChan <- err: @@ -358,7 +358,7 @@ func (rhs *RoutingHistoryStore) Count() (int, error) { count := 0 scanner := bufio.NewScanner(file) - + buf := make([]byte, 0, 64*1024) scanner.Buffer(buf, 1024*1024) diff --git a/internal/memory/structure.go b/internal/memory/structure.go index 2c02e36c..af8c86b6 100644 --- a/internal/memory/structure.go +++ b/internal/memory/structure.go @@ -13,7 +13,7 @@ const ( userPreferencesDir = "user-preferences" dailyLogsDir = "daily" analyticsDir = "analytics" - + // File permissions dirPermissions = 0700 // Owner read/write/execute only filePermissions = 0600 // Owner read/write only diff --git a/internal/observability/events.go b/internal/observability/events.go index b5e6572d..2478f8df 100644 --- a/internal/observability/events.go +++ b/internal/observability/events.go @@ -184,9 +184,9 @@ func GinMiddleware(emitter *EventEmitter) gin.HandlerFunc { if provider == "" { provider = "unknown" } - + autoRouted := c.Writer.Header().Get("X-Auto-Routed") == "true" - + var rqs float64 if rqsStr := c.Writer.Header().Get("X-RQS"); rqsStr != "" { if parsed, err := strconv.ParseFloat(rqsStr, 64); err == nil { diff --git a/internal/observability/metrics.go b/internal/observability/metrics.go index 736191f1..f2b756c9 100644 --- a/internal/observability/metrics.go +++ b/internal/observability/metrics.go @@ -10,10 +10,10 @@ import ( "time" "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" + log "github.com/sirupsen/logrus" ) var ( @@ -170,7 +170,7 @@ func RegisterMetricsRoute(router *gin.Engine, enabled bool, path string) { if !enabled || path == "" { return } - + // Add prometheus handler to global router router.GET(path, gin.WrapH(promhttp.Handler())) log.Infof("Registered Prometheus metrics endpoint at %s", path) diff --git a/internal/performance/circuit/breaker.go b/internal/performance/circuit/breaker.go index 35a9565b..2a900e30 100644 --- a/internal/performance/circuit/breaker.go +++ b/internal/performance/circuit/breaker.go @@ -180,7 +180,7 @@ func (r *Registry) Get(provider string) *Breaker { if b, ok := r.breakers[provider]; ok { return b } - + b := NewBreaker(r.config) r.breakers[provider] = b return b diff --git a/internal/performance/circuit/breaker_test.go b/internal/performance/circuit/breaker_test.go index 32d9c148..925c96a4 100644 --- a/internal/performance/circuit/breaker_test.go +++ b/internal/performance/circuit/breaker_test.go @@ -67,10 +67,10 @@ func TestCircuitBreaker_StateTransitions(t *testing.T) { // test failure from half-open goes back to open instantly cb.RecordFailure() cb.RecordFailure() - cb.RecordFailure() // trip again + cb.RecordFailure() // trip again time.Sleep(150 * time.Millisecond) // wait - _ = cb.AllowRequest() // transition to half-open - cb.RecordFailure() // fail the probe + _ = cb.AllowRequest() // transition to half-open + cb.RecordFailure() // fail the probe if cb.State() != circuit.StateOpen { t.Fatalf("expected state to return to open after failed probe, got %v", cb.State()) } diff --git a/internal/performance/loadshed/middleware.go b/internal/performance/loadshed/middleware.go index 74d17ae4..84d9d433 100644 --- a/internal/performance/loadshed/middleware.go +++ b/internal/performance/loadshed/middleware.go @@ -34,8 +34,8 @@ func Middleware(cfg config.LoadSheddingConfig) gin.HandlerFunc { retryAfter := fmt.Sprintf("%d", cfg.RetryAfterSeconds) log.WithFields(log.Fields{ - "max_in_flight": cfg.MaxInFlight, - "retry_after_secs": cfg.RetryAfterSeconds, + "max_in_flight": cfg.MaxInFlight, + "retry_after_secs": cfg.RetryAfterSeconds, }).Info("Load shedding enabled") return func(c *gin.Context) { diff --git a/internal/performance/ratelimit/limiter.go b/internal/performance/ratelimit/limiter.go index 2009d1df..0efce884 100644 --- a/internal/performance/ratelimit/limiter.go +++ b/internal/performance/ratelimit/limiter.go @@ -109,9 +109,9 @@ func Middleware(cfg config.RateLimiterConfig) gin.HandlerFunc { limiter := New(cfg) log.WithFields(log.Fields{ - "global_rps": cfg.GlobalRequestsPerSecond, - "global_burst": cfg.GlobalBurst, - "per_key_rps": cfg.PerKeyRequestsPerSecond, + "global_rps": cfg.GlobalRequestsPerSecond, + "global_burst": cfg.GlobalBurst, + "per_key_rps": cfg.PerKeyRequestsPerSecond, "per_key_burst": cfg.PerKeyBurst, }).Info("Rate limiter enabled") diff --git a/internal/plugin/lua_engine.go b/internal/plugin/lua_engine.go index 8425beff..749aeb22 100644 --- a/internal/plugin/lua_engine.go +++ b/internal/plugin/lua_engine.go @@ -587,38 +587,38 @@ func (e *LuaEngine) registerSwitchAIModule(L *lua.LState) { L.Push(lua.LString("missing arguments: req (cmd, args, env, toolName, toolArgs)")) return 2 } - + cmdName := L.CheckString(1) - + argsTbl := L.CheckTable(2) var args []string argsTbl.ForEach(func(_, v lua.LValue) { args = append(args, v.String()) }) - + envTbl := L.CheckTable(3) var env = make(map[string]string) envTbl.ForEach(func(k, v lua.LValue) { env[k.String()] = v.String() }) - + toolName := L.CheckString(4) - + toolArgsTbl := L.CheckTable(5) toolArgs := e.luaTableToGoMap(toolArgsTbl) - + ctx := L.Context() if ctx == nil { ctx = context.Background() } - + output, err := executeMCPToolCall(ctx, cmdName, args, env, toolName, toolArgs) if err != nil { - L.Push(lua.LString(output)) + L.Push(lua.LString(output)) L.Push(lua.LString(err.Error())) return 2 } - + L.Push(lua.LString(output)) L.Push(lua.LNil) // No error return 2 @@ -677,7 +677,7 @@ func (e *LuaEngine) registerSwitchAIModule(L *lua.LState) { if urlStr, ok := imgUrlData["url"].(string); ok { // Call Minimax VLM natively vlmReqBody, _ := json.Marshal(map[string]any{ - "prompt": prompt, + "prompt": prompt, "image_url": urlStr, }) @@ -685,10 +685,10 @@ func (e *LuaEngine) registerSwitchAIModule(L *lua.LState) { if err == nil { req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+apiKey) - + client := &http.Client{Timeout: 60 * time.Second} resp, err := client.Do(req) - + newContent := map[string]any{"type": "text"} if err != nil { newContent["text"] = "[Image extraction failed network error: " + err.Error() + "]" @@ -739,23 +739,25 @@ func (e *LuaEngine) registerSwitchAIModule(L *lua.LState) { L.Push(lua.LString("missing arguments")) return 2 } - + bodyStr := L.CheckString(1) cmdName := L.CheckString(2) - + argsTbl := L.CheckTable(3) var args []string argsTbl.ForEach(func(_, v lua.LValue) { args = append(args, v.String()) }) - + envTbl := L.CheckTable(4) var env = make(map[string]string) envTbl.ForEach(func(k, v lua.LValue) { env[k.String()] = v.String() }) - + toolName := L.CheckString(5) prompt := L.CheckString(6) ctx := L.Context() - if ctx == nil { ctx = context.Background() } + if ctx == nil { + ctx = context.Background() + } // Parse body natively in Go var payload map[string]any @@ -775,15 +777,23 @@ func (e *LuaEngine) registerSwitchAIModule(L *lua.LState) { for i, msgVal := range msgsVal { msg, ok := msgVal.(map[string]any) - if !ok { continue } - if role, _ := msg["role"].(string); role != "user" { continue } + if !ok { + continue + } + if role, _ := msg["role"].(string); role != "user" { + continue + } contents, ok := msg["content"].([]any) - if !ok { continue } + if !ok { + continue + } for j, contentVal := range contents { content, ok := contentVal.(map[string]any) - if !ok { continue } + if !ok { + continue + } if t, _ := content["type"].(string); t == "image_url" { if imgUrlData, ok := content["image_url"].(map[string]any); ok { @@ -791,7 +801,7 @@ func (e *LuaEngine) registerSwitchAIModule(L *lua.LState) { // Execute MCP tool natively in Go toolArgs := map[string]any{"prompt": prompt, "image_source": urlStr} out, err := executeMCPToolCall(ctx, cmdName, args, env, toolName, toolArgs) - + newContent := map[string]any{"type": "text"} if err != nil { newContent["text"] = "[Image extraction failed internally: " + err.Error() + "]" diff --git a/internal/plugin/mcp_client.go b/internal/plugin/mcp_client.go index 7818db6d..6f3bf6e9 100644 --- a/internal/plugin/mcp_client.go +++ b/internal/plugin/mcp_client.go @@ -39,7 +39,7 @@ func executeMCPToolCall(ctx context.Context, command string, args []string, env } cmd := exec.CommandContext(ctx, command, args...) - + // Set up environment safely cmd.Env = os.Environ() for k, v := range env { @@ -50,7 +50,7 @@ func executeMCPToolCall(ctx context.Context, command string, args []string, env if err != nil { return "", fmt.Errorf("failed to get stdin pipe: %w", err) } - + stdout, err := cmd.StdoutPipe() if err != nil { return "", fmt.Errorf("failed to get stdout pipe: %w", err) @@ -86,7 +86,7 @@ func executeMCPToolCall(ctx context.Context, command string, args []string, env if err := decoder.Decode(&resp); err != nil { return nil, fmt.Errorf("JSON-RPC decoding error: %w", err) } - + if resp.ID == expectedID { return &resp, nil } diff --git a/internal/runtime/executor/cli_executor.go b/internal/runtime/executor/cli_executor.go index 3f70398f..5541d8da 100644 --- a/internal/runtime/executor/cli_executor.go +++ b/internal/runtime/executor/cli_executor.go @@ -521,11 +521,14 @@ func (e *LocalCLIExecutor) executeRemote(ctx context.Context, remoteHost, binary return switchailocalexecutor.Response{}, fmt.Errorf("invalid remote host URL, must use http or https scheme and include a host") } - targetURL := *parsedURL - targetURL.Path = strings.TrimSuffix(targetURL.Path, "/") + "/run" - targetURL.RawPath = "" - targetURL.Fragment = "" - targetURL.RawFragment = "" + // Validate remote host URL to prevent SSRF + targetURL := url.URL{ + Scheme: parsedURL.Scheme, + Host: parsedURL.Host, + User: parsedURL.User, + Path: strings.TrimSuffix(parsedURL.Path, "/") + "/run", + RawQuery: parsedURL.RawQuery, + } httpReq, err := http.NewRequestWithContext(ctx, "POST", targetURL.String(), bytes.NewReader(jsonBody)) if err != nil { diff --git a/internal/steering/injection_test.go b/internal/steering/injection_test.go index e447aef2..6e6a4511 100644 --- a/internal/steering/injection_test.go +++ b/internal/steering/injection_test.go @@ -92,4 +92,4 @@ func TestContextInjector_FormatContextInjection(t *testing.T) { // Test empty template result = injector.FormatContextInjection("", ctx) assert.Equal(t, "", result) -} \ No newline at end of file +} diff --git a/internal/superbrain/doctor/doctor_test.go b/internal/superbrain/doctor/doctor_test.go index b7dc2770..5fdf3055 100644 --- a/internal/superbrain/doctor/doctor_test.go +++ b/internal/superbrain/doctor/doctor_test.go @@ -21,10 +21,10 @@ func TestInternalDoctor_Diagnose_PatternMatch(t *testing.T) { doctor := NewInternalDoctor(cfg) tests := []struct { - name string - snapshot *types.DiagnosticSnapshot - wantType types.FailureType - wantMatched bool + name string + snapshot *types.DiagnosticSnapshot + wantType types.FailureType + wantMatched bool }{ { name: "permission prompt detected", diff --git a/internal/superbrain/executor.go b/internal/superbrain/executor.go index 51a6f621..508b9c40 100644 --- a/internal/superbrain/executor.go +++ b/internal/superbrain/executor.go @@ -311,7 +311,6 @@ func (se *SuperbrainExecutor) CountTokens(ctx context.Context, auth *sdkauth.Aut return se.wrapped.CountTokens(ctx, auth, req, opts) } - // isEnabled checks if Superbrain is enabled. func (se *SuperbrainExecutor) isEnabled() bool { se.mu.RLock() @@ -342,12 +341,12 @@ func (se *SuperbrainExecutor) shouldPerformPreFlight() bool { if se.config == nil { return false } - + // Check if Context Sculptor is enabled via component flags if !se.config.ComponentFlags.SculptorEnabled { return false } - + return se.config.ContextSculptor.Enabled && se.contextSculptor != nil } @@ -429,10 +428,10 @@ func (se *SuperbrainExecutor) performPreFlight(ctx context.Context, req switchai // Log that we're performing optimization log.WithFields(log.Fields{ - "model": modelName, + "model": modelName, "estimated_tokens": analysis.EstimatedTokens, - "context_limit": analysis.ModelContextLimit, - "file_count": analysis.FileCount, + "context_limit": analysis.ModelContextLimit, + "file_count": analysis.FileCount, }).Info("Content exceeds context limit, performing optimization") // Build file list for optimization diff --git a/internal/superbrain/executor_test.go b/internal/superbrain/executor_test.go index 98b932bc..a68d59e9 100644 --- a/internal/superbrain/executor_test.go +++ b/internal/superbrain/executor_test.go @@ -15,11 +15,11 @@ import ( // mockExecutor is a mock implementation of ProviderExecutor for testing. type mockExecutor struct { - identifier string - executeFunc func(ctx context.Context, auth *sdkauth.Auth, req switchailocalexecutor.Request, opts switchailocalexecutor.Options) (switchailocalexecutor.Response, error) + identifier string + executeFunc func(ctx context.Context, auth *sdkauth.Auth, req switchailocalexecutor.Request, opts switchailocalexecutor.Options) (switchailocalexecutor.Response, error) executeStreamFunc func(ctx context.Context, auth *sdkauth.Auth, req switchailocalexecutor.Request, opts switchailocalexecutor.Options) (<-chan switchailocalexecutor.StreamChunk, error) - refreshFunc func(ctx context.Context, auth *sdkauth.Auth) (*sdkauth.Auth, error) - countTokensFunc func(ctx context.Context, auth *sdkauth.Auth, req switchailocalexecutor.Request, opts switchailocalexecutor.Options) (switchailocalexecutor.Response, error) + refreshFunc func(ctx context.Context, auth *sdkauth.Auth) (*sdkauth.Auth, error) + countTokensFunc func(ctx context.Context, auth *sdkauth.Auth, req switchailocalexecutor.Request, opts switchailocalexecutor.Options) (switchailocalexecutor.Response, error) } func (m *mockExecutor) Identifier() string { diff --git a/internal/superbrain/injector/audit_test.go b/internal/superbrain/injector/audit_test.go index 5e110036..1df11cfb 100644 --- a/internal/superbrain/injector/audit_test.go +++ b/internal/superbrain/injector/audit_test.go @@ -36,11 +36,11 @@ func TestAuditLogging(t *testing.T) { } tests := []struct { - name string - logContent string - requestID string - provider string - model string + name string + logContent string + requestID string + provider string + model string expectedOutcome string }{ { diff --git a/internal/superbrain/metadata/metadata.go b/internal/superbrain/metadata/metadata.go index 5562dc02..48f1a728 100644 --- a/internal/superbrain/metadata/metadata.go +++ b/internal/superbrain/metadata/metadata.go @@ -13,15 +13,15 @@ import ( // Aggregator manages the collection of healing actions and metadata for a request. // It provides thread-safe methods for recording actions and building the final metadata. type Aggregator struct { - mu sync.RWMutex - requestID string - originalProvider string - finalProvider string - startTime time.Time - actions []types.HealingAction - contextOptimized bool - highDensityMap *types.HighDensityMap - diagnosisHistory []*types.Diagnosis + mu sync.RWMutex + requestID string + originalProvider string + finalProvider string + startTime time.Time + actions []types.HealingAction + contextOptimized bool + highDensityMap *types.HighDensityMap + diagnosisHistory []*types.Diagnosis } // NewAggregator creates a new metadata aggregator for a request. diff --git a/internal/superbrain/metadata/metadata_test.go b/internal/superbrain/metadata/metadata_test.go index f0fb5803..508d8742 100644 --- a/internal/superbrain/metadata/metadata_test.go +++ b/internal/superbrain/metadata/metadata_test.go @@ -36,7 +36,7 @@ func TestRecordAction(t *testing.T) { // Record a successful action agg.RecordAction("stdin_injection", "Injected permission response", true, map[string]interface{}{ - "pattern": "file_permission", + "pattern": "file_permission", "response": "y\n", }) @@ -106,10 +106,10 @@ func TestRecordDiagnosis(t *testing.T) { agg := NewAggregator("req-3", "claudecli") diagnosis := &types.Diagnosis{ - FailureType: types.FailureTypePermissionPrompt, - RootCause: "Process waiting for file permission", - Confidence: 0.95, - Remediation: types.RemediationStdinInject, + FailureType: types.FailureTypePermissionPrompt, + RootCause: "Process waiting for file permission", + Confidence: 0.95, + Remediation: types.RemediationStdinInject, RemediationArgs: map[string]string{ "pattern": "file_permission", }, diff --git a/internal/superbrain/overwatch/ring_buffer.go b/internal/superbrain/overwatch/ring_buffer.go index ec90f117..9918cc87 100644 --- a/internal/superbrain/overwatch/ring_buffer.go +++ b/internal/superbrain/overwatch/ring_buffer.go @@ -11,12 +11,12 @@ import ( // It maintains a fixed-size buffer that overwrites the oldest entries when full, // ensuring memory-bounded log retention for diagnostic purposes. type RingBuffer struct { - mu sync.RWMutex - buffer []string - size int // Maximum capacity of the buffer - head int // Index where next write will occur - count int // Current number of elements in buffer - isFull bool // Whether the buffer has wrapped around + mu sync.RWMutex + buffer []string + size int // Maximum capacity of the buffer + head int // Index where next write will occur + count int // Current number of elements in buffer + isFull bool // Whether the buffer has wrapped around } // NewRingBuffer creates a new RingBuffer with the specified capacity. diff --git a/internal/superbrain/recovery/restart.go b/internal/superbrain/recovery/restart.go index 730875bc..d17b4a8b 100644 --- a/internal/superbrain/recovery/restart.go +++ b/internal/superbrain/recovery/restart.go @@ -111,7 +111,7 @@ func (rm *RestartManager) GetRestartStrategy(diagnosis *types.Diagnosis, provide // Get the corrective flags for this failure type flags, ok := providerFlags.Flags[diagnosis.FailureType] if !ok || len(flags) == 0 { - strategy.Reason = fmt.Sprintf("No corrective flags defined for failure type %s on provider %s", + strategy.Reason = fmt.Sprintf("No corrective flags defined for failure type %s on provider %s", diagnosis.FailureType, provider) return strategy } @@ -144,7 +144,7 @@ func (rm *RestartManager) BuildRestartCommand(originalArgs []string, correctiveF // Build new args list newArgs := make([]string, 0, len(originalArgs)+len(correctiveFlags)) - + // Copy original args newArgs = append(newArgs, originalArgs...) @@ -160,11 +160,11 @@ func (rm *RestartManager) BuildRestartCommand(originalArgs []string, correctiveF // FormatRestartAction creates a HealingAction for a restart attempt. func FormatRestartAction(strategy *RestartStrategy, success bool, details map[string]interface{}) types.HealingAction { - description := fmt.Sprintf("Restarted process with corrective flags: %s", + description := fmt.Sprintf("Restarted process with corrective flags: %s", strings.Join(strategy.CorrectiveFlags, " ")) - + if !success { - description = fmt.Sprintf("Attempted restart with corrective flags: %s (failed)", + description = fmt.Sprintf("Attempted restart with corrective flags: %s (failed)", strings.Join(strategy.CorrectiveFlags, " ")) } @@ -254,7 +254,7 @@ func (rm *RestartManager) DecideRestartOrEscalate( // Check if restart limit has been reached if restartCount >= maxRestarts { decision.ShouldEscalate = true - decision.Reason = fmt.Sprintf("Restart limit reached (%d/%d), escalating to fallback", + decision.Reason = fmt.Sprintf("Restart limit reached (%d/%d), escalating to fallback", restartCount, maxRestarts) return decision } @@ -272,7 +272,7 @@ func (rm *RestartManager) DecideRestartOrEscalate( // Can restart decision.ShouldRestart = true - decision.Reason = fmt.Sprintf("Restart attempt %d/%d with corrective flags", + decision.Reason = fmt.Sprintf("Restart attempt %d/%d with corrective flags", restartCount+1, maxRestarts) return decision } @@ -285,7 +285,7 @@ func CheckRestartLimit(restartCount, maxRestarts int) bool { // FormatEscalationAction creates a HealingAction for escalation to fallback. func FormatEscalationAction(reason string, restartCount, maxRestarts int) types.HealingAction { - description := fmt.Sprintf("Escalating to fallback after %d restart attempts (max: %d)", + description := fmt.Sprintf("Escalating to fallback after %d restart attempts (max: %d)", restartCount, maxRestarts) return types.HealingAction{ @@ -364,9 +364,9 @@ func (rh *RestartHistory) ToHealingActions() []types.HealingAction { for _, attempt := range rh.Attempts { action := types.HealingAction{ - ActionType: "restart_with_flags", - Description: fmt.Sprintf("Restart attempt %d/%d with flags: %s", - attempt.AttemptNumber, rh.TotalRestarts, + ActionType: "restart_with_flags", + Description: fmt.Sprintf("Restart attempt %d/%d with flags: %s", + attempt.AttemptNumber, rh.TotalRestarts, strings.Join(attempt.Strategy.CorrectiveFlags, " ")), Success: attempt.Success, Details: map[string]interface{}{ @@ -400,7 +400,7 @@ func (rh *RestartHistory) GetSummary() string { // RecordSuccessfulRestart creates a HealingAction for a successful restart with corrective flags. // This is a convenience function that includes all relevant metadata about the successful restart. func RecordSuccessfulRestart(strategy *RestartStrategy, attemptNumber int, durationMs int64) types.HealingAction { - description := fmt.Sprintf("Successfully restarted process (attempt %d) with corrective flags: %s", + description := fmt.Sprintf("Successfully restarted process (attempt %d) with corrective flags: %s", attemptNumber, strings.Join(strategy.CorrectiveFlags, " ")) return types.HealingAction{ @@ -419,7 +419,7 @@ func RecordSuccessfulRestart(strategy *RestartStrategy, attemptNumber int, durat // RecordFailedRestart creates a HealingAction for a failed restart attempt. func RecordFailedRestart(strategy *RestartStrategy, attemptNumber int, errorMsg string, durationMs int64) types.HealingAction { - description := fmt.Sprintf("Restart attempt %d failed with corrective flags: %s", + description := fmt.Sprintf("Restart attempt %d failed with corrective flags: %s", attemptNumber, strings.Join(strategy.CorrectiveFlags, " ")) return types.HealingAction{ diff --git a/internal/superbrain/recovery/restart_test.go b/internal/superbrain/recovery/restart_test.go index 3e2e928c..f262edb1 100644 --- a/internal/superbrain/recovery/restart_test.go +++ b/internal/superbrain/recovery/restart_test.go @@ -487,14 +487,14 @@ func TestDecideRestartOrEscalate(t *testing.T) { rm := NewRestartManager() tests := []struct { - name string - diagnosis *types.Diagnosis - provider string - restartCount int - maxRestarts int - expectRestart bool - expectEscalate bool - reasonContains string + name string + diagnosis *types.Diagnosis + provider string + restartCount int + maxRestarts int + expectRestart bool + expectEscalate bool + reasonContains string }{ { name: "First restart attempt - should restart", diff --git a/internal/superbrain/router/adapter_test.go b/internal/superbrain/router/adapter_test.go index 71fcfebe..9cd3b077 100644 --- a/internal/superbrain/router/adapter_test.go +++ b/internal/superbrain/router/adapter_test.go @@ -402,7 +402,6 @@ func TestIsCLIProvider(t *testing.T) { } } - func TestFallbackRouter_GetFallbackWithAdaptation(t *testing.T) { cfg := &config.FallbackConfig{ Enabled: true, diff --git a/internal/superbrain/sculptor/analyzer.go b/internal/superbrain/sculptor/analyzer.go index 6a060feb..610c9c54 100644 --- a/internal/superbrain/sculptor/analyzer.go +++ b/internal/superbrain/sculptor/analyzer.go @@ -182,7 +182,6 @@ func (fa *FileAnalyzer) analyzeReference(path string) FileReference { return ref } - // scanDirectory recursively scans a directory and estimates total tokens. // Returns the total estimated tokens and file count. func (fa *FileAnalyzer) scanDirectory(dirPath string) (int, int) { diff --git a/internal/superbrain/sculptor/analyzer_test.go b/internal/superbrain/sculptor/analyzer_test.go index c33d0123..ec151577 100644 --- a/internal/superbrain/sculptor/analyzer_test.go +++ b/internal/superbrain/sculptor/analyzer_test.go @@ -31,7 +31,7 @@ func TestDetectFileReferences(t *testing.T) { t.Run("detects relative paths", func(t *testing.T) { content := "Please analyze ./src/main.go and ./README.md" refs := fa.DetectFileReferences(content) - + paths := make(map[string]bool) for _, ref := range refs { paths[ref.Path] = true @@ -225,7 +225,7 @@ func TestFileReference(t *testing.T) { t.Run("analyzes existing file", func(t *testing.T) { refs := fa.DetectFileReferences("Check sample.txt") - + var found *FileReference for i := range refs { if refs[i].Path == "sample.txt" { @@ -253,7 +253,7 @@ func TestFileReference(t *testing.T) { t.Run("handles non-existent file", func(t *testing.T) { refs := fa.DetectFileReferences("Check nonexistent.go") - + var found *FileReference for i := range refs { if refs[i].Path == "nonexistent.go" { diff --git a/internal/superbrain/security/failsafe_test.go b/internal/superbrain/security/failsafe_test.go index 81763ab2..7aeb7d9c 100644 --- a/internal/superbrain/security/failsafe_test.go +++ b/internal/superbrain/security/failsafe_test.go @@ -332,10 +332,10 @@ func TestCreateSafeFailureResponse(t *testing.T) { func TestCreateSafeFailureResponse_WithVariousForbiddenOperations(t *testing.T) { testCases := []struct { - name string - forbiddenOps []string - diagnosis *types.Diagnosis - expectedInReason string + name string + forbiddenOps []string + diagnosis *types.Diagnosis + expectedInReason string }{ { name: "file_delete operation", diff --git a/internal/superbrain/types/types.go b/internal/superbrain/types/types.go index f81371d0..4e35520b 100644 --- a/internal/superbrain/types/types.go +++ b/internal/superbrain/types/types.go @@ -51,7 +51,6 @@ type HealingMetadata struct { DiagnosisHistory []*Diagnosis `json:"diagnosis_history,omitempty"` } - // ToOpenAIExtension formats the healing metadata for inclusion in OpenAI-compatible responses. // This allows clients to see what autonomous actions were taken without breaking API compatibility. func (h *HealingMetadata) ToOpenAIExtension() map[string]interface{} { diff --git a/internal/translator/gemini/openai/responses/gemini_openai_responses_streaming_test.go b/internal/translator/gemini/openai/responses/gemini_openai_responses_streaming_test.go index 2c76c1c5..0d8bc2d6 100644 --- a/internal/translator/gemini/openai/responses/gemini_openai_responses_streaming_test.go +++ b/internal/translator/gemini/openai/responses/gemini_openai_responses_streaming_test.go @@ -67,13 +67,13 @@ func TestConvertGeminiResponseToOpenAIResponses(t *testing.T) { t.Fatal("Expected events for second chunk, got none") } - foundDone := false - for _, event := range events2 { - if strings.Contains(event, "response.output_text.done") { - foundDone = true - } - } - if !foundDone { - t.Error("Did not find response.output_text.done event") - } + foundDone := false + for _, event := range events2 { + if strings.Contains(event, "response.output_text.done") { + foundDone = true + } + } + if !foundDone { + t.Error("Did not find response.output_text.done event") + } } diff --git a/internal/util/statebox.go b/internal/util/statebox.go index 6576e87c..bcd4d07b 100644 --- a/internal/util/statebox.go +++ b/internal/util/statebox.go @@ -18,10 +18,10 @@ import ( // It provides centralized path resolution for all mutable application data, // ensuring consistent handling of environment variables and preventing ghost directories. type StateBox struct { - rootPath string - readOnly bool - legacyAuthDir string // For backward compatibility with auth-dir config - mu sync.RWMutex + rootPath string + readOnly bool + legacyAuthDir string // For backward compatibility with auth-dir config + mu sync.RWMutex } // NewStateBox creates a new StateBox instance. diff --git a/internal/util/think_tag_normalizer.go b/internal/util/think_tag_normalizer.go index aa8f41f0..fd5763dc 100644 --- a/internal/util/think_tag_normalizer.go +++ b/internal/util/think_tag_normalizer.go @@ -22,7 +22,7 @@ import "strings" // - No tag: returns ("", original, false) — zero overhead path // - Unclosed tag: treats everything after as thinking // - Multiple blocks: concatenates all thinking text -// - Empty : found=true but thinking="" +// - Empty : found=true but thinking="" // - Whitespace around tags and in content is preserved internally but // leading/trailing whitespace on both outputs is trimmed func ExtractThinkTags(content string) (thinking string, cleaned string, found bool) { diff --git a/internal/watcher/watcher.go b/internal/watcher/watcher.go index a018cf6e..6a2afda4 100644 --- a/internal/watcher/watcher.go +++ b/internal/watcher/watcher.go @@ -33,29 +33,29 @@ type authDirProvider interface { // Watcher manages file watching for configuration and authentication files type Watcher struct { - configPath string - authDir string - config *config.Config - clientsMutex sync.RWMutex - configReloadMu sync.Mutex - configReloadTimer *time.Timer - reloadCallback func(*config.Config) + configPath string + authDir string + config *config.Config + clientsMutex sync.RWMutex + configReloadMu sync.Mutex + configReloadTimer *time.Timer + reloadCallback func(*config.Config) superbrainReloadCallback func(*config.SuperbrainConfig) - watcher *fsnotify.Watcher - lastAuthHashes map[string]string - lastRemoveTimes map[string]time.Time - lastConfigHash string - authQueue chan<- AuthUpdate - currentAuths map[string]*coreauth.Auth - runtimeAuths map[string]*coreauth.Auth - dispatchMu sync.Mutex - dispatchCond *sync.Cond - pendingUpdates map[string]AuthUpdate - pendingOrder []string - dispatchCancel context.CancelFunc - storePersister storePersister - mirroredAuthDir string - oldConfigYaml []byte + watcher *fsnotify.Watcher + lastAuthHashes map[string]string + lastRemoveTimes map[string]time.Time + lastConfigHash string + authQueue chan<- AuthUpdate + currentAuths map[string]*coreauth.Auth + runtimeAuths map[string]*coreauth.Auth + dispatchMu sync.Mutex + dispatchCond *sync.Cond + pendingUpdates map[string]AuthUpdate + pendingOrder []string + dispatchCancel context.CancelFunc + storePersister storePersister + mirroredAuthDir string + oldConfigYaml []byte } // AuthUpdateAction represents the type of change detected in auth sources. diff --git a/sdk/api/handlers/handlers_circuit_breaker_test.go b/sdk/api/handlers/handlers_circuit_breaker_test.go index ea80e1fd..7d2b53be 100644 --- a/sdk/api/handlers/handlers_circuit_breaker_test.go +++ b/sdk/api/handlers/handlers_circuit_breaker_test.go @@ -13,7 +13,7 @@ import ( func TestCircuitBreaker_PipelineIntegration(t *testing.T) { sdkCfg := &sdkconfig.SDKConfig{} - + cfg := &config.CircuitBreakerConfig{ Enabled: true, FailureThreshold: 2, @@ -21,13 +21,13 @@ func TestCircuitBreaker_PipelineIntegration(t *testing.T) { HalfOpenMax: 1, } breaker := circuit.NewRegistry(cfg) - + manager := coreauth.NewManager(nil, nil, nil) - + h := NewBaseAPIHandlers(sdkCfg, manager, nil, nil, nil, breaker) - + providers := []string{"fail_provider1", "fail_provider2"} - + // 1. All providers initially healthy allowed, err := h.enforceCircuitBreaker(providers) if err != nil { @@ -36,11 +36,11 @@ func TestCircuitBreaker_PipelineIntegration(t *testing.T) { if len(allowed) != 2 { t.Fatalf("expected 2 providers allowed, got %v", len(allowed)) } - + // 2. Trip one provider breaker.Get("fail_provider1").RecordFailure() breaker.Get("fail_provider1").RecordFailure() // Trips - + allowed, err = h.enforceCircuitBreaker(providers) if err != nil { t.Fatalf("expected nil err, got %v", err) @@ -48,11 +48,11 @@ func TestCircuitBreaker_PipelineIntegration(t *testing.T) { if len(allowed) != 1 || allowed[0] != "fail_provider2" { t.Fatalf("expected fail_provider2, got %v", allowed) } - + // 3. Trip second provider breaker.Get("fail_provider2").RecordFailure() breaker.Get("fail_provider2").RecordFailure() // Trips - + allowed, err = h.enforceCircuitBreaker(providers) if err == nil { t.Fatalf("expected error when all circuits are open") @@ -63,7 +63,7 @@ func TestCircuitBreaker_PipelineIntegration(t *testing.T) { if len(allowed) != 0 { t.Fatalf("expected 0 allowed providers, got %v", len(allowed)) } - + // 4. Wait for reset timeout and test probes time.Sleep(150 * time.Millisecond) allowed, err = h.enforceCircuitBreaker(providers) diff --git a/sdk/api/handlers/notifications/notifications_test.go b/sdk/api/handlers/notifications/notifications_test.go index 34ee5108..e5debd26 100644 --- a/sdk/api/handlers/notifications/notifications_test.go +++ b/sdk/api/handlers/notifications/notifications_test.go @@ -405,8 +405,8 @@ func TestLookupTelegramBot(t *testing.T) { } func TestIsChatAllowed(t *testing.T) { - open := config.TelegramBot{} // empty allowlist => any - restricted := config.TelegramBot{AllowedChatIDs: []int64{1, 2}} // explicit + open := config.TelegramBot{} // empty allowlist => any + restricted := config.TelegramBot{AllowedChatIDs: []int64{1, 2}} // explicit if !open.IsChatAllowed(999) { t.Errorf("open bot must allow any chat_id") } diff --git a/sdk/switchailocal/auth/conductor.go b/sdk/switchailocal/auth/conductor.go index 16962299..80379a4f 100644 --- a/sdk/switchailocal/auth/conductor.go +++ b/sdk/switchailocal/auth/conductor.go @@ -563,7 +563,7 @@ func (m *Manager) executeWithProvider(ctx context.Context, provider string, req continue } m.MarkResult(execCtx, result) - + if opts.Metadata != nil { opts.Metadata["executed_provider"] = provider } @@ -631,7 +631,7 @@ func (m *Manager) executeCountWithProvider(ctx context.Context, provider string, continue } m.MarkResult(execCtx, result) - + if resp.Metadata == nil { resp.Metadata = make(map[string]any) } @@ -720,11 +720,11 @@ func (m *Manager) executeStreamWithProvider(ctx context.Context, provider string m.MarkResult(streamCtx, Result{AuthID: streamAuth.ID, Provider: streamProvider, Model: routeModel, Success: true}) } }(execCtx, auth.Clone(), provider, chunks) - + if opts.Metadata != nil { opts.Metadata["executed_provider"] = provider } - + return out, nil } } diff --git a/sdk/switchailocal/auth/conductor_failover_test.go b/sdk/switchailocal/auth/conductor_failover_test.go index d31e66a0..46470d19 100644 --- a/sdk/switchailocal/auth/conductor_failover_test.go +++ b/sdk/switchailocal/auth/conductor_failover_test.go @@ -20,7 +20,7 @@ type httpStatusErr struct { msg string } -func (e *httpStatusErr) Error() string { return e.msg } +func (e *httpStatusErr) Error() string { return e.msg } func (e *httpStatusErr) StatusCode() int { return e.code } // TestExecuteProvidersOnce_AdvancesOnTransient verifies the conductor