From 649283f2c213a02e7ca591cbe6337492ba4a52c0 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:35:57 +0000 Subject: [PATCH 1/3] Add sync.Pool for ResponseWriterWrapper buffer Co-authored-by: rschumann <360788+rschumann@users.noreply.github.com> --- .jules/bolt.md | 3 + cmd/server/main.go | 6 +- internal/access/config_access/provider.go | 8 +- .../access/config_access/provider_test.go | 4 +- internal/api/middleware/request_logging.go | 1 + internal/api/middleware/response_writer.go | 21 +- 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/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 +- 91 files changed, 1031 insertions(+), 1009 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 5049e2f8..8eefefae 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -219,3 +219,6 @@ go tool pprof mem.prof Remember: You're Bolt, making switchAILocal lightning fast. But speed without correctness is useless. Measure, optimize, verify. **If you can't find a clear performance win today, stop and do not create a PR.** +## $(date +%Y-%m-%d) - Add sync.Pool for ResponseWriterWrapper buffer +**Learning:** In the hot path of HTTP request handling (`internal/api/middleware/response_writer.go`), a new `bytes.Buffer` was being allocated for every non-streaming response. This results in significant memory allocation overhead for high-throughput API gateways. +**Action:** Implemented a `sync.Pool` for `bytes.Buffer` and added a `Release()` method to recycle buffers after the request completes. Included a capacity check (`Cap() <= 128*1024`) before returning the buffer to the pool to prevent memory bloat from occasionally large responses. 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/middleware/request_logging.go b/internal/api/middleware/request_logging.go index 7593f759..61f0c2f1 100644 --- a/internal/api/middleware/request_logging.go +++ b/internal/api/middleware/request_logging.go @@ -65,6 +65,7 @@ func RequestLoggingMiddleware(logger logging.RequestLogger) gin.HandlerFunc { // Error is logged but we continue serving the response _ = err } + wrapper.Release() } } diff --git a/internal/api/middleware/response_writer.go b/internal/api/middleware/response_writer.go index 52ef09b2..80cd7f7b 100644 --- a/internal/api/middleware/response_writer.go +++ b/internal/api/middleware/response_writer.go @@ -11,12 +11,20 @@ import ( "bytes" "net/http" "strings" + "sync" "github.com/gin-gonic/gin" "github.com/traylinx/switchAILocal/internal/interfaces" "github.com/traylinx/switchAILocal/internal/logging" ) +// bufferPool is a sync.Pool for bytes.Buffer to reduce allocations during response logging. +var bufferPool = sync.Pool{ + New: func() interface{} { + return &bytes.Buffer{} + }, +} + // RequestInfo holds essential details of an incoming HTTP request for logging purposes. type RequestInfo struct { URL string // URL is the request URL. @@ -55,7 +63,7 @@ type ResponseWriterWrapper struct { func NewResponseWriterWrapper(w gin.ResponseWriter, logger logging.RequestLogger, requestInfo *RequestInfo) *ResponseWriterWrapper { return &ResponseWriterWrapper{ ResponseWriter: w, - body: &bytes.Buffer{}, + body: bufferPool.Get().(*bytes.Buffer), logger: logger, requestInfo: requestInfo, headers: make(map[string][]string), @@ -241,6 +249,17 @@ func (w *ResponseWriterWrapper) processStreamingChunks(done chan struct{}) { } } +// Release returns the buffer to the pool. It should be called after Finalize. +func (w *ResponseWriterWrapper) Release() { + if w.body != nil { + if w.body.Cap() <= 128*1024 { // 128KB limit to avoid memory bloat + w.body.Reset() + bufferPool.Put(w.body) + } + w.body = nil + } +} + // Finalize completes the logging process for the request and response. // For streaming responses, it closes the chunk channel and the stream writer. // For non-streaming responses, it logs the complete request and response details, 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/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 From 2813a08d53a26add7967986f76157f0ffc2d54b3 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 14:08:39 +0000 Subject: [PATCH 2/3] Bump Go to 1.26.11 to fix GO-2026-6859 Co-authored-by: rschumann <360788+rschumann@users.noreply.github.com> --- .github/workflows/go-vulnerability-gate.yml | 2 +- .jules/bolt.md | 4 ++++ go.mod | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go-vulnerability-gate.yml b/.github/workflows/go-vulnerability-gate.yml index 8efccd59..a9157aab 100644 --- a/.github/workflows/go-vulnerability-gate.yml +++ b/.github/workflows/go-vulnerability-gate.yml @@ -24,7 +24,7 @@ jobs: run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - uses: actions/setup-go@v6 with: - go-version: '1.26.10' + go-version: '1.26.11' cache: true - run: go install golang.org/x/vuln/cmd/govulncheck@v1.9.0 - run: govulncheck ./... diff --git a/.jules/bolt.md b/.jules/bolt.md index 8eefefae..b2e899d4 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -222,3 +222,7 @@ Remember: You're Bolt, making switchAILocal lightning fast. But speed without co ## $(date +%Y-%m-%d) - Add sync.Pool for ResponseWriterWrapper buffer **Learning:** In the hot path of HTTP request handling (`internal/api/middleware/response_writer.go`), a new `bytes.Buffer` was being allocated for every non-streaming response. This results in significant memory allocation overhead for high-throughput API gateways. **Action:** Implemented a `sync.Pool` for `bytes.Buffer` and added a `Release()` method to recycle buffers after the request completes. Included a capacity check (`Cap() <= 128*1024`) before returning the buffer to the pool to prevent memory bloat from occasionally large responses. + +## $(date +%Y-%m-%d) - Bump Go Version for Vulnerability Fix +**Learning:** Found a vulnerability (GO-2026-6859) in the Go standard library (`net/http`) while running `govulncheck`. The vulnerability is an HTTP/1 server crash due to a MaxBytesReader header race. +**Action:** Bumped the Go version in `go.mod` and `.github/workflows/go-vulnerability-gate.yml` from `1.26.10` to `1.26.11` to resolve the vulnerability. This ensures the CI check for govulncheck passes. diff --git a/go.mod b/go.mod index b1325805..cd601151 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/traylinx/switchAILocal -go 1.26.10 +go 1.26.11 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 From 87523a590ea5dc0d1729ab16891302af1666c9ba 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 15:09:17 +0000 Subject: [PATCH 3/3] Bump Go to 1.26.11 to fix GO-2026-6859 Co-authored-by: rschumann <360788+rschumann@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 2 +- .jules/bolt.md | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c432c789..b0406fe1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ on: branches: [main] env: - GO_VERSION: '1.26.10' + GO_VERSION: '1.26.11' jobs: build: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3417c682..ddfd8c8f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,7 +57,7 @@ jobs: - uses: actions/setup-go@v6 with: - go-version: '1.26.10' + go-version: '1.26.11' cache: true - name: Verify scanned checkout diff --git a/.jules/bolt.md b/.jules/bolt.md index b2e899d4..a507dcf2 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -226,3 +226,7 @@ Remember: You're Bolt, making switchAILocal lightning fast. But speed without co ## $(date +%Y-%m-%d) - Bump Go Version for Vulnerability Fix **Learning:** Found a vulnerability (GO-2026-6859) in the Go standard library (`net/http`) while running `govulncheck`. The vulnerability is an HTTP/1 server crash due to a MaxBytesReader header race. **Action:** Bumped the Go version in `go.mod` and `.github/workflows/go-vulnerability-gate.yml` from `1.26.10` to `1.26.11` to resolve the vulnerability. This ensures the CI check for govulncheck passes. + +## $(date +%Y-%m-%d) - Bump Go Version in CI Files +**Learning:** Bumping the Go version in `go.mod` to resolve vulnerabilities requires also updating the Go version specified in all CI workflow files (like `ci.yml` and `release.yml`) to ensure the CI uses the correct toolchain and passes build checks. +**Action:** Updated `GO_VERSION` in `.github/workflows/ci.yml` and `go-version` in `.github/workflows/release.yml` from `1.26.10` to `1.26.11` to fix CI failures.