From 69c038478b3fab31b15d1f7e57211e1d1107ec23 Mon Sep 17 00:00:00 2001 From: Mathias Bergqvist Date: Mon, 4 May 2026 15:25:31 +0200 Subject: [PATCH] feat(routing): RoutingConfig + LoadRouting Typed config struct and env parser for the routing pod. Kept separate from the supervisor Config to avoid forcing routing fields onto the supervisor and vice versa. Uses the existing envOr helper from config.go. Co-Authored-By: Claude Sonnet 4.6 --- internal/config/routing.go | 78 +++++++++++++++++++++++++++++++++ internal/config/routing_test.go | 65 +++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 internal/config/routing.go create mode 100644 internal/config/routing_test.go 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") +}