diff options
Diffstat (limited to 'internal/config/config.go')
| -rw-r--r-- | internal/config/config.go | 89 |
1 files changed, 86 insertions, 3 deletions
diff --git a/internal/config/config.go b/internal/config/config.go index 5e1e518..8c21c6c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -21,6 +21,7 @@ type Config struct { UpstreamHTTP UpstreamHTTPConfig `json:"upstream_http"` Providers []ProviderConfig `json:"providers"` Models []ModelConfig `json:"models"` + Billing BillingConfig `json:"billing"` Observability ObservabilityConfig `json:"observability"` } @@ -74,9 +75,13 @@ type ProviderConfig struct { } type ModelConfig struct { - ID string `json:"id"` - OwnedBy string `json:"owned_by"` - Routes []RouteConfig `json:"routes"` + ID string `json:"id"` + OwnedBy string `json:"owned_by"` + InputPriceMicrosPerMillion int64 `json:"input_price_micros_per_million"` + OutputPriceMicrosPerMillion int64 `json:"output_price_micros_per_million"` + CacheReadPriceMicrosPerMillion int64 `json:"cache_read_price_micros_per_million"` + CacheWritePriceMicrosPerMillion int64 `json:"cache_write_price_micros_per_million"` + Routes []RouteConfig `json:"routes"` } type RouteConfig struct { @@ -91,6 +96,25 @@ type ObservabilityConfig struct { ExposeMetrics bool `json:"expose_metrics"` } +type BillingConfig struct { + Enabled bool `json:"enabled"` + Currency string `json:"currency"` + DefaultMaxOutputTokens int64 `json:"default_max_output_tokens"` + MinTopUpMinor int64 `json:"min_top_up_minor"` + MaxTopUpMinor int64 `json:"max_top_up_minor"` + Stripe StripeConfig `json:"stripe"` +} + +type StripeConfig struct { + Enabled bool `json:"enabled"` + APIKeyEnv string `json:"api_key_env"` + WebhookSecretEnv string `json:"webhook_secret_env"` + SuccessURL string `json:"success_url"` + CancelURL string `json:"cancel_url"` + APIKey string `json:"-"` + WebhookSecret string `json:"-"` +} + func Load(path string) (Config, error) { f, err := os.Open(path) if err != nil { @@ -179,6 +203,24 @@ func applyDefaults(cfg *Config) { if cfg.Observability.UsageBuffer == 0 { cfg.Observability.UsageBuffer = 8192 } + if cfg.Billing.Currency == "" { + cfg.Billing.Currency = "usd" + } + if cfg.Billing.DefaultMaxOutputTokens == 0 { + cfg.Billing.DefaultMaxOutputTokens = 4096 + } + if cfg.Billing.MaxTopUpMinor == 0 { + cfg.Billing.MaxTopUpMinor = 1000000 + } + if cfg.Billing.MinTopUpMinor == 0 { + cfg.Billing.MinTopUpMinor = 500 + } + if cfg.Billing.Stripe.APIKeyEnv == "" { + cfg.Billing.Stripe.APIKeyEnv = "AIGW_STRIPE_API_KEY" + } + if cfg.Billing.Stripe.WebhookSecretEnv == "" { + cfg.Billing.Stripe.WebhookSecretEnv = "AIGW_STRIPE_WEBHOOK_SECRET" + } for i := range cfg.Models { for j := range cfg.Models[i].Routes { if cfg.Models[i].Routes[j].Weight == 0 { @@ -197,6 +239,10 @@ func resolveSecrets(cfg *Config) error { if cfg.Admin.Enabled { cfg.Admin.Token = os.Getenv(cfg.Admin.TokenEnv) } + if cfg.Billing.Enabled && cfg.Billing.Stripe.Enabled { + cfg.Billing.Stripe.APIKey = os.Getenv(cfg.Billing.Stripe.APIKeyEnv) + cfg.Billing.Stripe.WebhookSecret = os.Getenv(cfg.Billing.Stripe.WebhookSecretEnv) + } for i := range cfg.Providers { provider := &cfg.Providers[i] if provider.APIKeyEnv == "" { @@ -240,6 +286,40 @@ func Validate(cfg Config) error { return errors.New("admin.base_path must start with / and cannot be /") } } + if cfg.Billing.Enabled { + if !cfg.ControlPlane.Enabled { + return errors.New("billing requires control_plane.enabled") + } + if cfg.Auth.AllowAnonymous { + return errors.New("billing cannot be enabled with auth.allow_anonymous") + } + if len(cfg.Billing.Currency) != 3 || strings.ToLower(cfg.Billing.Currency) != cfg.Billing.Currency { + return errors.New("billing.currency must be a lowercase ISO 4217 code") + } + if cfg.Billing.DefaultMaxOutputTokens < 1 { + return errors.New("billing.default_max_output_tokens must be positive") + } + if cfg.Billing.MinTopUpMinor < 1 || cfg.Billing.MaxTopUpMinor < cfg.Billing.MinTopUpMinor { + return errors.New("billing top-up bounds are invalid") + } + if cfg.Billing.Stripe.Enabled { + if cfg.Billing.Stripe.APIKey == "" { + return fmt.Errorf("billing.stripe: environment variable %s is empty", cfg.Billing.Stripe.APIKeyEnv) + } + if cfg.Billing.Stripe.WebhookSecret == "" { + return fmt.Errorf("billing.stripe: environment variable %s is empty", cfg.Billing.Stripe.WebhookSecretEnv) + } + if strings.TrimSpace(cfg.Billing.Stripe.SuccessURL) == "" || strings.TrimSpace(cfg.Billing.Stripe.CancelURL) == "" { + return errors.New("billing.stripe.success_url and cancel_url are required") + } + for name, value := range map[string]string{"success_url": cfg.Billing.Stripe.SuccessURL, "cancel_url": cfg.Billing.Stripe.CancelURL} { + parsed, err := url.Parse(value) + if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") { + return fmt.Errorf("billing.stripe.%s must be an absolute http(s) URL", name) + } + } + } + } providers := make(map[string]ProviderConfig, len(cfg.Providers)) for _, provider := range cfg.Providers { @@ -274,6 +354,9 @@ func Validate(cfg Config) error { return fmt.Errorf("duplicate model id %q", model.ID) } models[model.ID] = struct{}{} + if model.InputPriceMicrosPerMillion < 0 || model.OutputPriceMicrosPerMillion < 0 || model.CacheReadPriceMicrosPerMillion < 0 || model.CacheWritePriceMicrosPerMillion < 0 { + return fmt.Errorf("model %q: prices cannot be negative", model.ID) + } if len(model.Routes) == 0 { return fmt.Errorf("model %q: at least one route is required", model.ID) } |
