diff --git a/internal/config/routing.go b/internal/config/routing.go new file mode 100644 index 0000000..0eebe15 --- /dev/null +++ b/internal/config/routing.go @@ -0,0 +1,78 @@ +package config + +import ( + "fmt" + "os" + "strconv" +) + +// RoutingConfig holds the runtime configuration for the routing pod. +// Separate from Config because the routing pod's surface differs from the supervisor's. +type RoutingConfig struct { + Port string // ROUTING_PORT, default 3210 + MCPAuthToken string // ROUTING_MCP_TOKEN, optional bearer token + LiteLLMBaseURL string // LITELLM_BASE_URL, default http://piguard:4000 + LiteLLMAPIKey string // LITELLM_API_KEY + BrainURL string // BRAIN_URL, default http://ingestion.supervisor:3300 + LocalModel string // HYPERGUILD_LOCAL_MODEL, default qwen35 + ClaudeModel string // HYPERGUILD_CLAUDE_MODEL, default claude-sonnet-4-6 + RouteLocalFloor float64 // HYPERGUILD_ROUTE_LOCAL_FLOOR, default 0.90 + RouteLocalCeil float64 // HYPERGUILD_ROUTE_LOCAL_CEIL, default 0.70 + PassRateTTLSeconds int // HYPERGUILD_PASS_RATE_TTL_SECONDS, default 60 +} + +func LoadRouting() (RoutingConfig, error) { + cfg := RoutingConfig{ + Port: envOr("ROUTING_PORT", "3210"), + MCPAuthToken: os.Getenv("ROUTING_MCP_TOKEN"), + LiteLLMBaseURL: envOr("LITELLM_BASE_URL", "http://piguard:4000"), + LiteLLMAPIKey: os.Getenv("LITELLM_API_KEY"), + BrainURL: envOr("BRAIN_URL", "http://ingestion.supervisor:3300"), + LocalModel: envOr("HYPERGUILD_LOCAL_MODEL", "qwen35"), + ClaudeModel: envOr("HYPERGUILD_CLAUDE_MODEL", "claude-sonnet-4-6"), + } + + floor, err := parseFloatEnv("HYPERGUILD_ROUTE_LOCAL_FLOOR", 0.90) + if err != nil { + return RoutingConfig{}, err + } + cfg.RouteLocalFloor = floor + + ceil, err := parseFloatEnv("HYPERGUILD_ROUTE_LOCAL_CEIL", 0.70) + if err != nil { + return RoutingConfig{}, err + } + cfg.RouteLocalCeil = ceil + + ttl, err := parseIntEnv("HYPERGUILD_PASS_RATE_TTL_SECONDS", 60) + if err != nil { + return RoutingConfig{}, err + } + cfg.PassRateTTLSeconds = ttl + + return cfg, nil +} + +func parseFloatEnv(key string, def float64) (float64, error) { + v := os.Getenv(key) + if v == "" { + return def, nil + } + f, err := strconv.ParseFloat(v, 64) + if err != nil { + return 0, fmt.Errorf("config: %s: %w", key, err) + } + return f, nil +} + +func parseIntEnv(key string, def int) (int, error) { + v := os.Getenv(key) + if v == "" { + return def, nil + } + n, err := strconv.Atoi(v) + if err != nil { + return 0, fmt.Errorf("config: %s: %w", key, err) + } + return n, nil +} diff --git a/internal/config/routing_test.go b/internal/config/routing_test.go new file mode 100644 index 0000000..6e2a7fa --- /dev/null +++ b/internal/config/routing_test.go @@ -0,0 +1,65 @@ +package config_test + +import ( + "testing" + + "github.com/mathiasbq/supervisor/internal/config" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLoadRoutingDefaults(t *testing.T) { + for _, k := range []string{ + "ROUTING_PORT", "ROUTING_MCP_TOKEN", "LITELLM_BASE_URL", "LITELLM_API_KEY", + "BRAIN_URL", "HYPERGUILD_LOCAL_MODEL", "HYPERGUILD_CLAUDE_MODEL", + "HYPERGUILD_ROUTE_LOCAL_FLOOR", "HYPERGUILD_ROUTE_LOCAL_CEIL", + "HYPERGUILD_PASS_RATE_TTL_SECONDS", + } { + t.Setenv(k, "") + } + + cfg, err := config.LoadRouting() + require.NoError(t, err) + assert.Equal(t, "3210", cfg.Port) + assert.Equal(t, "", cfg.MCPAuthToken) + assert.Equal(t, "http://piguard:4000", cfg.LiteLLMBaseURL) + assert.Equal(t, "http://ingestion.supervisor:3300", cfg.BrainURL) + assert.Equal(t, "qwen35", cfg.LocalModel) + assert.Equal(t, "claude-sonnet-4-6", cfg.ClaudeModel) + assert.InDelta(t, 0.90, cfg.RouteLocalFloor, 1e-9) + assert.InDelta(t, 0.70, cfg.RouteLocalCeil, 1e-9) + assert.Equal(t, 60, cfg.PassRateTTLSeconds) +} + +func TestLoadRoutingFromEnv(t *testing.T) { + t.Setenv("ROUTING_PORT", "3250") + t.Setenv("ROUTING_MCP_TOKEN", "tok-xyz") + t.Setenv("LITELLM_BASE_URL", "http://localhost:4000") + t.Setenv("LITELLM_API_KEY", "lk") + t.Setenv("BRAIN_URL", "http://localhost:3300") + t.Setenv("HYPERGUILD_LOCAL_MODEL", "qwen2-7b") + t.Setenv("HYPERGUILD_CLAUDE_MODEL", "claude-opus-4-7") + t.Setenv("HYPERGUILD_ROUTE_LOCAL_FLOOR", "0.85") + t.Setenv("HYPERGUILD_ROUTE_LOCAL_CEIL", "0.65") + t.Setenv("HYPERGUILD_PASS_RATE_TTL_SECONDS", "30") + + cfg, err := config.LoadRouting() + require.NoError(t, err) + assert.Equal(t, "3250", cfg.Port) + assert.Equal(t, "tok-xyz", cfg.MCPAuthToken) + assert.Equal(t, "http://localhost:4000", cfg.LiteLLMBaseURL) + assert.Equal(t, "lk", cfg.LiteLLMAPIKey) + assert.Equal(t, "http://localhost:3300", cfg.BrainURL) + assert.Equal(t, "qwen2-7b", cfg.LocalModel) + assert.Equal(t, "claude-opus-4-7", cfg.ClaudeModel) + assert.InDelta(t, 0.85, cfg.RouteLocalFloor, 1e-9) + assert.InDelta(t, 0.65, cfg.RouteLocalCeil, 1e-9) + assert.Equal(t, 30, cfg.PassRateTTLSeconds) +} + +func TestLoadRoutingRejectsBadFloat(t *testing.T) { + t.Setenv("HYPERGUILD_ROUTE_LOCAL_FLOOR", "not-a-number") + _, err := config.LoadRouting() + require.Error(t, err) + assert.Contains(t, err.Error(), "HYPERGUILD_ROUTE_LOCAL_FLOOR") +}