mirror of
https://github.com/lukaszraczylo/mcp-captcharesolver.git
synced 2026-10-12 00:45:28 +00:00
feat(config): env-driven config with validation
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
// Package config loads and validates the MCP server configuration from env vars.
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config is the validated runtime configuration. Fields are ordered
|
||||
// largest-alignment-first to satisfy the fieldalignment linter.
|
||||
type Config struct {
|
||||
Provider string
|
||||
BaseURL string
|
||||
APIKey string
|
||||
Model string
|
||||
AudioModel string
|
||||
LogLevel string
|
||||
Temperature float64
|
||||
Timeout time.Duration
|
||||
MaxTokens int
|
||||
}
|
||||
|
||||
var validProviders = map[string]bool{
|
||||
"anthropic": true, "openai": true, "openai-compatible": true, "gemini": true,
|
||||
}
|
||||
|
||||
// Load reads CAPTCHA_LLM_* env vars and returns a validated Config.
|
||||
func Load() (Config, error) {
|
||||
c := Config{
|
||||
Provider: os.Getenv("CAPTCHA_LLM_PROVIDER"),
|
||||
BaseURL: os.Getenv("CAPTCHA_LLM_BASE_URL"),
|
||||
APIKey: os.Getenv("CAPTCHA_LLM_API_KEY"),
|
||||
Model: os.Getenv("CAPTCHA_LLM_MODEL"),
|
||||
AudioModel: os.Getenv("CAPTCHA_LLM_AUDIO_MODEL"),
|
||||
LogLevel: envOr("CAPTCHA_LOG_LEVEL", "info"),
|
||||
MaxTokens: 1024,
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
if !validProviders[c.Provider] {
|
||||
return Config{}, fmt.Errorf("CAPTCHA_LLM_PROVIDER must be one of anthropic|openai|openai-compatible|gemini, got %q", c.Provider)
|
||||
}
|
||||
if c.APIKey == "" && c.BaseURL == "" {
|
||||
return Config{}, fmt.Errorf("CAPTCHA_LLM_API_KEY is required (or set CAPTCHA_LLM_BASE_URL for a keyless local endpoint)")
|
||||
}
|
||||
if c.Model == "" {
|
||||
return Config{}, fmt.Errorf("CAPTCHA_LLM_MODEL is required")
|
||||
}
|
||||
if v := os.Getenv("CAPTCHA_LLM_TEMPERATURE"); v != "" {
|
||||
f, err := strconv.ParseFloat(v, 64)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("CAPTCHA_LLM_TEMPERATURE: %w", err)
|
||||
}
|
||||
c.Temperature = f
|
||||
}
|
||||
if v := os.Getenv("CAPTCHA_LLM_MAX_TOKENS"); v != "" {
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("CAPTCHA_LLM_MAX_TOKENS: %w", err)
|
||||
}
|
||||
c.MaxTokens = n
|
||||
}
|
||||
if v := os.Getenv("CAPTCHA_LLM_TIMEOUT"); v != "" {
|
||||
d, err := time.ParseDuration(v)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("CAPTCHA_LLM_TIMEOUT: %w", err)
|
||||
}
|
||||
c.Timeout = d
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func envOr(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLoad(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
env map[string]string
|
||||
want Config
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid anthropic",
|
||||
env: map[string]string{
|
||||
"CAPTCHA_LLM_PROVIDER": "anthropic",
|
||||
"CAPTCHA_LLM_API_KEY": "k",
|
||||
"CAPTCHA_LLM_MODEL": "claude-opus-4-8",
|
||||
},
|
||||
want: Config{
|
||||
Provider: "anthropic", APIKey: "k", Model: "claude-opus-4-8",
|
||||
LogLevel: "info", MaxTokens: 1024, Timeout: 60 * time.Second,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing model",
|
||||
env: map[string]string{"CAPTCHA_LLM_PROVIDER": "openai", "CAPTCHA_LLM_API_KEY": "k"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "bad provider",
|
||||
env: map[string]string{"CAPTCHA_LLM_PROVIDER": "nope", "CAPTCHA_LLM_API_KEY": "k", "CAPTCHA_LLM_MODEL": "m"},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
for k, v := range tc.env {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
got, err := Load()
|
||||
if tc.wantErr {
|
||||
if err == nil {
|
||||
t.Fatal("want error, got nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Fatalf("got %+v want %+v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user