The #35 data gate could never fill: a real review call routed cleanly to qwen36 but /pass-rate stayed total:0 under every key. Root cause was three independent defects in the session_log path, each alone fatal: - A: a successful routed call logged final_status "skip", never "pass". /pass-rate computes pass/(pass+fail) and skips count as neither, so the >=0.90 gate was mathematically unreachable. Success now logs "pass". - B: every record was written under skill "_routing", so /pass-rate?skill= review|debug (what #35 measures) always read zero. Now uses the real e.Skill; routing decisions stay groupable via session_id "_routing". - C: the session_log POST to the bearer-gated ingestion /mcp carried no Authorization header → silent 401, swallowed by best-effort logging (the documented mcpclient-empty-token-silent-401 footgun). Logger now takes a token (BRAIN_MCP_TOKEN) and sets the bearer when non-empty. Tests rewritten to assert correct behavior (they had encoded the bugs: "skip" on success, "_routing" skill). New test covers the auth header and the empty-token path. Infra (BRAIN_MCP_TOKEN ExternalSecret + env on the routing deployment) and redeploy follow separately. Refs #73, #35. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
104 lines
3.7 KiB
Go
104 lines
3.7 KiB
Go
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 https://llm-api.d-ma.be
|
|
LiteLLMAPIKey string // LITELLM_API_KEY
|
|
BrainURL string // BRAIN_URL, default http://ingestion.supervisor:3300
|
|
BrainMCPToken string // BRAIN_MCP_TOKEN, bearer for the auth-gated ingestion /mcp (session_log)
|
|
FastModel string // HYPERGUILD_FAST_MODEL, default koala/qwen35-9b-fast
|
|
ThinkingModel string // HYPERGUILD_THINKING_MODEL, default iguana/gemma4-26b
|
|
// RouteLocalFloor and RouteLocalCeil intentionally invert the usual
|
|
// floor < ceil mathematical convention: Floor (default 0.90) is the
|
|
// UPPER boundary — at/above it, always route local; Ceil (default 0.70)
|
|
// is the LOWER boundary — below it, always route Claude. The band in
|
|
// between is the 50/50 sample zone. The naming follows the spec's policy
|
|
// vocabulary; see internal/routing/policy.go for the consumer.
|
|
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
|
|
|
|
// project_create configuration. Empty GiteaMCPURL disables the
|
|
// project_create tool registration so the routing pod still starts
|
|
// in environments where it's not wired up.
|
|
GiteaMCPURL string // GITEA_MCP_URL, e.g. http://koala:30340/mcp
|
|
GiteaMCPToken string // GITEA_MCP_TOKEN, bearer for gitea-mcp
|
|
GiteaOwner string // GITEA_OWNER, default mathias
|
|
GitHubOwner string // GITHUB_OWNER, default mathiasb
|
|
InfraRepo string // INFRA_REPO, default infra
|
|
GitHubPAT string // GITHUB_PAT, repo scope; never logged
|
|
}
|
|
|
|
func LoadRouting() (RoutingConfig, error) {
|
|
cfg := RoutingConfig{
|
|
Port: envOr("ROUTING_PORT", "3210"),
|
|
MCPAuthToken: os.Getenv("ROUTING_MCP_TOKEN"),
|
|
LiteLLMBaseURL: envOr("LITELLM_BASE_URL", "https://llm-api.d-ma.be"),
|
|
LiteLLMAPIKey: os.Getenv("LITELLM_API_KEY"),
|
|
BrainURL: envOr("BRAIN_URL", "http://ingestion.supervisor:3300"),
|
|
BrainMCPToken: os.Getenv("BRAIN_MCP_TOKEN"),
|
|
FastModel: envOr("HYPERGUILD_FAST_MODEL", "koala/qwen35-9b-fast"),
|
|
ThinkingModel: envOr("HYPERGUILD_THINKING_MODEL", "iguana/gemma4-26b"),
|
|
}
|
|
|
|
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
|
|
|
|
cfg.GiteaMCPURL = os.Getenv("GITEA_MCP_URL")
|
|
cfg.GiteaMCPToken = os.Getenv("GITEA_MCP_TOKEN")
|
|
cfg.GiteaOwner = envOr("GITEA_OWNER", "mathias")
|
|
cfg.GitHubOwner = envOr("GITHUB_OWNER", "mathiasb")
|
|
cfg.InfraRepo = envOr("INFRA_REPO", "infra")
|
|
cfg.GitHubPAT = os.Getenv("GITHUB_PAT")
|
|
|
|
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
|
|
}
|