diff --git a/internal/tier/tier.go b/internal/tier/tier.go new file mode 100644 index 0000000..7758ac9 --- /dev/null +++ b/internal/tier/tier.go @@ -0,0 +1,64 @@ +// internal/tier/tier.go +package tier + +import ( + "context" + "net/http" + "time" +) + +// Tier represents the current operating capability level. +type Tier int + +const ( + Full Tier = 1 // internet + Anthropic API reachable + LANOnly Tier = 2 // LiteLLM on LAN reachable, no internet + Airplane Tier = 3 // no network +) + +// Info describes the current operating tier. +type Info struct { + Tier Tier `json:"tier"` + Label string `json:"label"` + AvailableModels []string `json:"available_models"` + ManagedAgents bool `json:"managed_agents"` +} + +// Detect probes the Anthropic endpoint and LiteLLM and returns the current tier. +// Each probe has a 2-second timeout. +func Detect(ctx context.Context, anthropicProbe, liteLLMBaseURL string) Info { + client := &http.Client{Timeout: 2 * time.Second} + + if probe(ctx, client, anthropicProbe) { + return Info{ + Tier: Full, + Label: "full-online", + ManagedAgents: true, + } + } + if probe(ctx, client, liteLLMBaseURL) { + return Info{ + Tier: LANOnly, + Label: "lan-only", + ManagedAgents: false, + } + } + return Info{ + Tier: Airplane, + Label: "airplane", + ManagedAgents: false, + } +} + +func probe(ctx context.Context, client *http.Client, url string) bool { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return false + } + resp, err := client.Do(req) + if err != nil { + return false + } + resp.Body.Close() + return true +} diff --git a/internal/tier/tier_test.go b/internal/tier/tier_test.go new file mode 100644 index 0000000..fdaed0f --- /dev/null +++ b/internal/tier/tier_test.go @@ -0,0 +1,48 @@ +// internal/tier/tier_test.go +package tier_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/mathiasbq/supervisor/internal/tier" + "github.com/stretchr/testify/assert" +) + +func TestDetect_Tier1_WhenBothReachable(t *testing.T) { + anthropic := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer anthropic.Close() + + litellm := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer litellm.Close() + + info := tier.Detect(context.Background(), anthropic.URL, litellm.URL) + assert.Equal(t, tier.Full, info.Tier) + assert.Equal(t, "full-online", info.Label) + assert.True(t, info.ManagedAgents) +} + +func TestDetect_Tier2_WhenOnlyLiteLLMReachable(t *testing.T) { + litellm := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer litellm.Close() + + info := tier.Detect(context.Background(), "http://127.0.0.1:1", litellm.URL) + assert.Equal(t, tier.LANOnly, info.Tier) + assert.Equal(t, "lan-only", info.Label) + assert.False(t, info.ManagedAgents) +} + +func TestDetect_Tier3_WhenNeitherReachable(t *testing.T) { + info := tier.Detect(context.Background(), "http://127.0.0.1:1", "http://127.0.0.1:2") + assert.Equal(t, tier.Airplane, info.Tier) + assert.Equal(t, "airplane", info.Label) + assert.False(t, info.ManagedAgents) +}