From 7958a14f0114f7a83d6fe96fa7f55f7675ead4e1 Mon Sep 17 00:00:00 2001 From: "Jakub A. W" Date: Mon, 9 Feb 2026 20:16:26 +0100 Subject: [PATCH 1/6] refactor: refactored a config file --- config/config.go | 164 ++++++++++++++-------------------- config/config_helpers_test.go | 6 +- 2 files changed, 68 insertions(+), 102 deletions(-) diff --git a/config/config.go b/config/config.go index 88c0cc3e..ebea4ad6 100644 --- a/config/config.go +++ b/config/config.go @@ -4,6 +4,7 @@ package config import ( "fmt" "os" + "reflect" "regexp" "strconv" "strings" @@ -39,75 +40,75 @@ type Config struct { // environment variables in internal/httpclient/client.go. type HTTPConfig struct { // Timeout is the overall HTTP request timeout in seconds (default: 600) - Timeout int `yaml:"timeout"` + Timeout int `yaml:"timeout" env:"HTTP_TIMEOUT"` // ResponseHeaderTimeout is the time to wait for response headers in seconds (default: 600) - ResponseHeaderTimeout int `yaml:"response_header_timeout"` + ResponseHeaderTimeout int `yaml:"response_header_timeout" env:"HTTP_RESPONSE_HEADER_TIMEOUT"` } // LogConfig holds audit logging configuration type LogConfig struct { // Enabled controls whether audit logging is active // Default: false - Enabled bool `yaml:"enabled"` + Enabled bool `yaml:"enabled" env:"LOGGING_ENABLED"` // LogBodies enables logging of full request/response bodies // WARNING: May contain sensitive data (PII, API keys in prompts) // Default: true - LogBodies bool `yaml:"log_bodies"` + LogBodies bool `yaml:"log_bodies" env:"LOGGING_LOG_BODIES"` // LogHeaders enables logging of request/response headers // Sensitive headers (Authorization, Cookie, etc.) are auto-redacted // Default: true - LogHeaders bool `yaml:"log_headers"` + LogHeaders bool `yaml:"log_headers" env:"LOGGING_LOG_HEADERS"` // BufferSize is the number of log entries to buffer before flushing // Default: 1000 - BufferSize int `yaml:"buffer_size"` + BufferSize int `yaml:"buffer_size" env:"LOGGING_BUFFER_SIZE"` // FlushInterval is how often to flush buffered logs (in seconds) // Default: 5 - FlushInterval int `yaml:"flush_interval"` + FlushInterval int `yaml:"flush_interval" env:"LOGGING_FLUSH_INTERVAL"` // RetentionDays is how long to keep logs (0 = forever) // Default: 30 - RetentionDays int `yaml:"retention_days"` + RetentionDays int `yaml:"retention_days" env:"LOGGING_RETENTION_DAYS"` // OnlyModelInteractions limits audit logging to AI model endpoints only // When true, only /v1/chat/completions and /v1/responses are logged // Endpoints like /health, /metrics, /admin, /v1/models are skipped // Default: true - OnlyModelInteractions bool `yaml:"only_model_interactions"` + OnlyModelInteractions bool `yaml:"only_model_interactions" env:"LOGGING_ONLY_MODEL_INTERACTIONS"` } // UsageConfig holds token usage tracking configuration type UsageConfig struct { // Enabled controls whether usage tracking is active // Default: true - Enabled bool `yaml:"enabled"` + Enabled bool `yaml:"enabled" env:"USAGE_ENABLED"` // EnforceReturningUsageData controls whether to enforce returning usage data in streaming responses. // When true, stream_options: {"include_usage": true} is automatically added to streaming requests. // Default: true - EnforceReturningUsageData bool `yaml:"enforce_returning_usage_data"` + EnforceReturningUsageData bool `yaml:"enforce_returning_usage_data" env:"ENFORCE_RETURNING_USAGE_DATA"` // BufferSize is the number of usage entries to buffer before flushing // Default: 1000 - BufferSize int `yaml:"buffer_size"` + BufferSize int `yaml:"buffer_size" env:"USAGE_BUFFER_SIZE"` // FlushInterval is how often to flush buffered usage entries (in seconds) // Default: 5 - FlushInterval int `yaml:"flush_interval"` + FlushInterval int `yaml:"flush_interval" env:"USAGE_FLUSH_INTERVAL"` // RetentionDays is how long to keep usage data (0 = forever) // Default: 90 - RetentionDays int `yaml:"retention_days"` + RetentionDays int `yaml:"retention_days" env:"USAGE_RETENTION_DAYS"` } // StorageConfig holds database storage configuration (used by audit logging, usage tracking, future IAM, etc.) type StorageConfig struct { // Type specifies the storage backend: "sqlite" (default), "postgresql", or "mongodb" - Type string `yaml:"type"` + Type string `yaml:"type" env:"STORAGE_TYPE"` // SQLite configuration SQLite SQLiteStorageConfig `yaml:"sqlite"` @@ -122,32 +123,32 @@ type StorageConfig struct { // SQLiteStorageConfig holds SQLite-specific storage configuration type SQLiteStorageConfig struct { // Path is the database file path (default: .cache/gomodel.db) - Path string `yaml:"path"` + Path string `yaml:"path" env:"SQLITE_PATH"` } // PostgreSQLStorageConfig holds PostgreSQL-specific storage configuration type PostgreSQLStorageConfig struct { // URL is the connection string (e.g., postgres://user:pass@localhost/dbname) - URL string `yaml:"url"` + URL string `yaml:"url" env:"POSTGRES_URL"` // MaxConns is the maximum connection pool size (default: 10) - MaxConns int `yaml:"max_conns"` + MaxConns int `yaml:"max_conns" env:"POSTGRES_MAX_CONNS"` } // MongoDBStorageConfig holds MongoDB-specific storage configuration type MongoDBStorageConfig struct { // URL is the connection string (e.g., mongodb://localhost:27017) - URL string `yaml:"url"` + URL string `yaml:"url" env:"MONGODB_URL"` // Database is the database name (default: gomodel) - Database string `yaml:"database"` + Database string `yaml:"database" env:"MONGODB_DATABASE"` } // CacheConfig holds cache configuration for model storage type CacheConfig struct { // Type specifies the cache backend: "local" (default) or "redis" - Type string `yaml:"type"` + Type string `yaml:"type" env:"CACHE_TYPE"` // CacheDir is the directory for local cache files (default: ".cache") - CacheDir string `yaml:"cache_dir"` + CacheDir string `yaml:"cache_dir" env:"GOMODEL_CACHE_DIR"` // Redis configuration (only used when Type is "redis") Redis RedisConfig `yaml:"redis"` @@ -156,31 +157,31 @@ type CacheConfig struct { // RedisConfig holds Redis-specific configuration type RedisConfig struct { // URL is the Redis connection URL (e.g., "redis://localhost:6379") - URL string `yaml:"url"` + URL string `yaml:"url" env:"REDIS_URL"` // Key is the Redis key for storing the model cache (default: "gomodel:models") - Key string `yaml:"key"` + Key string `yaml:"key" env:"REDIS_KEY"` // TTL is the time-to-live for cached data in seconds (default: 86400 = 24 hours) - TTL int `yaml:"ttl"` + TTL int `yaml:"ttl" env:"REDIS_TTL"` } // ServerConfig holds HTTP server configuration type ServerConfig struct { - Port string `yaml:"port"` - MasterKey string `yaml:"master_key"` // Optional: Master key for authentication - BodySizeLimit string `yaml:"body_size_limit"` // Max request body size (e.g., "10M", "1024K") + Port string `yaml:"port" env:"PORT"` + MasterKey string `yaml:"master_key" env:"GOMODEL_MASTER_KEY"` // Optional: Master key for authentication + BodySizeLimit string `yaml:"body_size_limit" env:"BODY_SIZE_LIMIT"` // Max request body size (e.g., "10M", "1024K") } // MetricsConfig holds observability configuration for Prometheus metrics type MetricsConfig struct { // Enabled controls whether Prometheus metrics are collected and exposed // Default: false - Enabled bool `yaml:"enabled"` + Enabled bool `yaml:"enabled" env:"METRICS_ENABLED"` // Endpoint is the HTTP path where metrics are exposed // Default: "/metrics" - Endpoint string `yaml:"endpoint"` + Endpoint string `yaml:"endpoint" env:"METRICS_ENDPOINT"` } // ProviderConfig holds generic provider configuration @@ -259,7 +260,7 @@ func Load() (*Config, error) { } // 4. Env vars always win - applyEnvVars(&cfg) + applyEnvOverrides(&cfg) // 5. Discover providers from env applyProviderEnvVars(&cfg) @@ -315,80 +316,45 @@ func applyYAML(cfg *Config) error { return nil } -// envStringMapping maps an environment variable to a Config string field setter. -type envStringMapping struct { - key string - set func(*Config, string) +// applyEnvOverrides walks cfg's struct fields and applies env var overrides +// based on `env` struct tags. Maps are skipped (providers are handled separately). +func applyEnvOverrides(cfg *Config) { + applyEnvOverridesValue(reflect.ValueOf(cfg).Elem()) } -// envBoolMapping maps an environment variable to a Config bool field setter. -type envBoolMapping struct { - key string - set func(*Config, bool) -} - -// envIntMapping maps an environment variable to a Config int field setter. -type envIntMapping struct { - key string - set func(*Config, int) -} - -var envStringMappings = []envStringMapping{ - {"PORT", func(c *Config, v string) { c.Server.Port = v }}, - {"GOMODEL_MASTER_KEY", func(c *Config, v string) { c.Server.MasterKey = v }}, - {"BODY_SIZE_LIMIT", func(c *Config, v string) { c.Server.BodySizeLimit = v }}, - {"CACHE_TYPE", func(c *Config, v string) { c.Cache.Type = v }}, - {"GOMODEL_CACHE_DIR", func(c *Config, v string) { c.Cache.CacheDir = v }}, - {"REDIS_URL", func(c *Config, v string) { c.Cache.Redis.URL = v }}, - {"REDIS_KEY", func(c *Config, v string) { c.Cache.Redis.Key = v }}, - {"STORAGE_TYPE", func(c *Config, v string) { c.Storage.Type = v }}, - {"SQLITE_PATH", func(c *Config, v string) { c.Storage.SQLite.Path = v }}, - {"POSTGRES_URL", func(c *Config, v string) { c.Storage.PostgreSQL.URL = v }}, - {"MONGODB_URL", func(c *Config, v string) { c.Storage.MongoDB.URL = v }}, - {"MONGODB_DATABASE", func(c *Config, v string) { c.Storage.MongoDB.Database = v }}, - {"METRICS_ENDPOINT", func(c *Config, v string) { c.Metrics.Endpoint = v }}, -} +func applyEnvOverridesValue(v reflect.Value) { + t := v.Type() + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + fieldVal := v.Field(i) -var envBoolMappings = []envBoolMapping{ - {"METRICS_ENABLED", func(c *Config, v bool) { c.Metrics.Enabled = v }}, - {"LOGGING_ENABLED", func(c *Config, v bool) { c.Logging.Enabled = v }}, - {"LOGGING_LOG_BODIES", func(c *Config, v bool) { c.Logging.LogBodies = v }}, - {"LOGGING_LOG_HEADERS", func(c *Config, v bool) { c.Logging.LogHeaders = v }}, - {"LOGGING_ONLY_MODEL_INTERACTIONS", func(c *Config, v bool) { c.Logging.OnlyModelInteractions = v }}, - {"USAGE_ENABLED", func(c *Config, v bool) { c.Usage.Enabled = v }}, - {"ENFORCE_RETURNING_USAGE_DATA", func(c *Config, v bool) { c.Usage.EnforceReturningUsageData = v }}, -} - -var envIntMappings = []envIntMapping{ - {"REDIS_TTL", func(c *Config, v int) { c.Cache.Redis.TTL = v }}, - {"POSTGRES_MAX_CONNS", func(c *Config, v int) { c.Storage.PostgreSQL.MaxConns = v }}, - {"LOGGING_BUFFER_SIZE", func(c *Config, v int) { c.Logging.BufferSize = v }}, - {"LOGGING_FLUSH_INTERVAL", func(c *Config, v int) { c.Logging.FlushInterval = v }}, - {"LOGGING_RETENTION_DAYS", func(c *Config, v int) { c.Logging.RetentionDays = v }}, - {"USAGE_BUFFER_SIZE", func(c *Config, v int) { c.Usage.BufferSize = v }}, - {"USAGE_FLUSH_INTERVAL", func(c *Config, v int) { c.Usage.FlushInterval = v }}, - {"USAGE_RETENTION_DAYS", func(c *Config, v int) { c.Usage.RetentionDays = v }}, - {"HTTP_TIMEOUT", func(c *Config, v int) { c.HTTP.Timeout = v }}, - {"HTTP_RESPONSE_HEADER_TIMEOUT", func(c *Config, v int) { c.HTTP.ResponseHeaderTimeout = v }}, -} + // Skip maps — providers are handled by applyProviderEnvVars + if field.Type.Kind() == reflect.Map { + continue + } + // Recurse into nested structs + if field.Type.Kind() == reflect.Struct { + applyEnvOverridesValue(fieldVal) + continue + } -// applyEnvVars applies environment variable overrides onto cfg. -// Only set env vars override; unset env vars leave cfg untouched. -func applyEnvVars(cfg *Config) { - for _, m := range envStringMappings { - if v := os.Getenv(m.key); v != "" { - m.set(cfg, v) + envKey := field.Tag.Get("env") + if envKey == "" { + continue } - } - for _, m := range envBoolMappings { - if v := os.Getenv(m.key); v != "" { - m.set(cfg, parseBool(v)) + envVal := os.Getenv(envKey) + if envVal == "" { + continue } - } - for _, m := range envIntMappings { - if v := os.Getenv(m.key); v != "" { - if n, err := strconv.Atoi(v); err == nil { - m.set(cfg, n) + + switch field.Type.Kind() { + case reflect.String: + fieldVal.SetString(envVal) + case reflect.Bool: + fieldVal.SetBool(parseBool(envVal)) + case reflect.Int: + if n, err := strconv.Atoi(envVal); err == nil { + fieldVal.SetInt(int64(n)) } } } diff --git a/config/config_helpers_test.go b/config/config_helpers_test.go index df1aa5e3..f11213a5 100644 --- a/config/config_helpers_test.go +++ b/config/config_helpers_test.go @@ -403,8 +403,8 @@ func TestRemoveEmptyProviders(t *testing.T) { } } -// TestApplyEnvVars tests the applyEnvVars function -func TestApplyEnvVars(t *testing.T) { +// TestApplyEnvOverrides tests the applyEnvOverrides function +func TestApplyEnvOverrides(t *testing.T) { tests := []struct { name string envVars map[string]string @@ -492,7 +492,7 @@ func TestApplyEnvVars(t *testing.T) { } cfg := defaultConfig() - applyEnvVars(&cfg) + applyEnvOverrides(&cfg) tt.check(t, &cfg) }) } From 4e05c16756472aee66dd6400b70ea60abd05b9e5 Mon Sep 17 00:00:00 2001 From: "Jakub A. W" Date: Mon, 9 Feb 2026 20:17:30 +0100 Subject: [PATCH 2/6] refactor: refactored a config even more :) --- .dockerignore | 1 + .env.template | 2 +- .gitignore | 3 +++ config/config.example.yaml | 2 +- config/config.go | 4 ++-- config/config_test.go | 4 ++-- docs/advanced/configuration.mdx | 4 ++-- internal/auditlog/factory.go | 2 +- internal/storage/sqlite.go | 2 +- internal/storage/storage.go | 4 ++-- internal/usage/factory.go | 2 +- 11 files changed, 17 insertions(+), 13 deletions(-) diff --git a/.dockerignore b/.dockerignore index 66ff6378..c55c2d9d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -65,6 +65,7 @@ prometheus.yml # Miscellaneous # =================== .cache/ +data/ *.bck.yml repomix-output.* session-*.md diff --git a/.env.template b/.env.template index 80532d5d..99f5bbd1 100644 --- a/.env.template +++ b/.env.template @@ -45,7 +45,7 @@ # STORAGE_TYPE=sqlite # SQLite Configuration (default, good for single instance) -# SQLITE_PATH=.cache/gomodel.db +# SQLITE_PATH=data/gomodel.db # PostgreSQL Configuration (for multi-instance deployments) # POSTGRES_URL=postgres://user:password@localhost:5432/gomodel diff --git a/.gitignore b/.gitignore index bd540d9a..bb06910b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ # Cache /.cache +# Data (runtime-created by SQLite storage) +/data/ + # Others /*.bck.yml /repomix-output.* diff --git a/config/config.example.yaml b/config/config.example.yaml index f696d97a..d03fcf0b 100644 --- a/config/config.example.yaml +++ b/config/config.example.yaml @@ -19,7 +19,7 @@ cache: storage: type: "sqlite" # "sqlite", "postgresql", or "mongodb" sqlite: - path: ".cache/gomodel.db" + path: "data/gomodel.db" postgresql: url: "postgres://user:pass@localhost/gomodel" max_conns: 10 diff --git a/config/config.go b/config/config.go index ebea4ad6..5b092ece 100644 --- a/config/config.go +++ b/config/config.go @@ -122,7 +122,7 @@ type StorageConfig struct { // SQLiteStorageConfig holds SQLite-specific storage configuration type SQLiteStorageConfig struct { - // Path is the database file path (default: .cache/gomodel.db) + // Path is the database file path (default: data/gomodel.db) Path string `yaml:"path" env:"SQLITE_PATH"` } @@ -207,7 +207,7 @@ func defaultConfig() Config { Storage: StorageConfig{ Type: "sqlite", SQLite: SQLiteStorageConfig{ - Path: ".cache/gomodel.db", + Path: "data/gomodel.db", }, PostgreSQL: PostgreSQLStorageConfig{ MaxConns: 10, diff --git a/config/config_test.go b/config/config_test.go index de4eef75..9a51aa9d 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -81,8 +81,8 @@ func TestDefaultConfig(t *testing.T) { if cfg.Storage.Type != "sqlite" { t.Errorf("expected Storage.Type=sqlite, got %s", cfg.Storage.Type) } - if cfg.Storage.SQLite.Path != ".cache/gomodel.db" { - t.Errorf("expected Storage.SQLite.Path=.cache/gomodel.db, got %s", cfg.Storage.SQLite.Path) + if cfg.Storage.SQLite.Path != "data/gomodel.db" { + t.Errorf("expected Storage.SQLite.Path=data/gomodel.db, got %s", cfg.Storage.SQLite.Path) } if cfg.Storage.PostgreSQL.MaxConns != 10 { t.Errorf("expected Storage.PostgreSQL.MaxConns=10, got %d", cfg.Storage.PostgreSQL.MaxConns) diff --git a/docs/advanced/configuration.mdx b/docs/advanced/configuration.mdx index 356570cf..d4535228 100644 --- a/docs/advanced/configuration.mdx +++ b/docs/advanced/configuration.mdx @@ -62,7 +62,7 @@ Storage is shared by audit logging, usage tracking, and future features like IAM | Variable | Description | Default | |----------|-------------|---------| | `STORAGE_TYPE` | Backend: `sqlite`, `postgresql`, or `mongodb` | `sqlite` | -| `SQLITE_PATH` | SQLite database file path | `.cache/gomodel.db` | +| `SQLITE_PATH` | SQLite database file path | `data/gomodel.db` | | `POSTGRES_URL` | PostgreSQL connection string | _(empty)_ | | `POSTGRES_MAX_CONNS` | PostgreSQL connection pool size | `10` | | `MONGODB_URL` | MongoDB connection string | _(empty)_ | @@ -285,7 +285,7 @@ All settings have sensible defaults. GOModel starts with zero configuration usin | Cache type | `local` | | Cache directory | `.cache` | | Storage type | `sqlite` | -| SQLite path | `.cache/gomodel.db` | +| SQLite path | `data/gomodel.db` | | Audit logging | Disabled | | Usage tracking | Enabled | | Metrics | Disabled | diff --git a/internal/auditlog/factory.go b/internal/auditlog/factory.go index a9a46700..6a3f9635 100644 --- a/internal/auditlog/factory.go +++ b/internal/auditlog/factory.go @@ -101,7 +101,7 @@ func buildStorageConfig(cfg *config.Config) storage.Config { storageCfg.Type = storage.TypeSQLite } if storageCfg.SQLite.Path == "" { - storageCfg.SQLite.Path = ".cache/gomodel.db" + storageCfg.SQLite.Path = "data/gomodel.db" } if storageCfg.MongoDB.Database == "" { storageCfg.MongoDB.Database = "gomodel" diff --git a/internal/storage/sqlite.go b/internal/storage/sqlite.go index fcf2f7e8..52c6579d 100644 --- a/internal/storage/sqlite.go +++ b/internal/storage/sqlite.go @@ -18,7 +18,7 @@ type sqliteStorage struct { // It enables WAL mode for better concurrent read/write performance. func NewSQLite(cfg SQLiteConfig) (Storage, error) { if cfg.Path == "" { - cfg.Path = ".cache/gomodel.db" + cfg.Path = "data/gomodel.db" } // Ensure directory exists diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 9731dd88..4a1775c3 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -33,7 +33,7 @@ type Config struct { // SQLiteConfig holds SQLite-specific configuration type SQLiteConfig struct { - // Path is the database file path (default: .cache/gomodel.db) + // Path is the database file path (default: data/gomodel.db) Path string } @@ -97,7 +97,7 @@ func DefaultConfig() Config { return Config{ Type: TypeSQLite, SQLite: SQLiteConfig{ - Path: ".cache/gomodel.db", + Path: "data/gomodel.db", }, PostgreSQL: PostgreSQLConfig{ MaxConns: 10, diff --git a/internal/usage/factory.go b/internal/usage/factory.go index fb6899a2..d677569d 100644 --- a/internal/usage/factory.go +++ b/internal/usage/factory.go @@ -132,7 +132,7 @@ func buildStorageConfig(cfg *config.Config) storage.Config { storageCfg.Type = storage.TypeSQLite } if storageCfg.SQLite.Path == "" { - storageCfg.SQLite.Path = ".cache/gomodel.db" + storageCfg.SQLite.Path = "data/gomodel.db" } if storageCfg.MongoDB.Database == "" { storageCfg.MongoDB.Database = "gomodel" From a7db359d19a4c1db6790b75b982c9c77564edf77 Mon Sep 17 00:00:00 2001 From: "Jakub A. W" Date: Mon, 9 Feb 2026 20:30:38 +0100 Subject: [PATCH 3/6] docs: updated configuration related docs --- docs/advanced/configuration.mdx | 175 +++++++++++++------------------- docs/docs.json | 2 +- 2 files changed, 74 insertions(+), 103 deletions(-) diff --git a/docs/advanced/configuration.mdx b/docs/advanced/configuration.mdx index d4535228..b6439505 100644 --- a/docs/advanced/configuration.mdx +++ b/docs/advanced/configuration.mdx @@ -3,33 +3,27 @@ title: "Configuration" description: "How to configure GOModel using environment variables, .env files, and YAML." --- -## Overview +## Good defaults -GOModel uses a three-layer configuration pipeline. Every setting has a sensible default, so you can start the server with zero configuration. +GOModel uses good defaults philosophy. It means that default settings should be enough to use it. + +## How to override the default settings? + +We use a three-layer configuration pipeline. Every setting has a sensible default, so you can start the server with zero configuration. ``` -Code Defaults → config.yaml (optional) → Environment Variables (always win) +Environment Variables (always win) → config.yaml (optional structured config) → Code Defaults ``` -Each layer overrides the previous one. Environment variables always take the highest priority. - - GOModel works out of the box with no configuration files. Just set your provider API keys as environment variables and start the server. - - -## Quick Start + As GOModel works out of the box with no configuration files you can try it in + a minute. -The fastest way to get running: +TODO: the link to the quick start should be provided here! -```bash -# Set at least one provider API key -export OPENAI_API_KEY="sk-..." - -# Start the server (uses default port 8080) -./gomodel -``` + -GOModel automatically discovers providers from well-known environment variables. No config file needed. +GOModel automatically discovers providers from well-known environment variables. ## Configuration Methods @@ -39,89 +33,91 @@ The most common way to configure GOModel. Set any of the variables below to over #### Server -| Variable | Description | Default | -|----------|-------------|---------| -| `PORT` | HTTP server port | `8080` | -| `GOMODEL_MASTER_KEY` | Authentication key for securing the gateway | _(empty, unsafe mode)_ | -| `BODY_SIZE_LIMIT` | Max request body size (e.g., `10M`, `1024K`, `500KB`) | _(no limit)_ | +| Variable | Description | Default | +| -------------------- | ----------------------------------------------------- | ---------------------- | +| `PORT` | HTTP server port | `8080` | +| `GOMODEL_MASTER_KEY` | Authentication key for securing the gateway | _(empty, unsafe mode)_ | +| `BODY_SIZE_LIMIT` | Max request body size (e.g., `10M`, `1024K`, `500KB`) | _(no limit)_ | #### Cache -| Variable | Description | Default | -|----------|-------------|---------| -| `CACHE_TYPE` | Cache backend: `local` or `redis` | `local` | -| `GOMODEL_CACHE_DIR` | Directory for local cache files | `.cache` | -| `REDIS_URL` | Redis connection URL | _(empty)_ | -| `REDIS_KEY` | Redis key for model cache | `gomodel:models` | -| `REDIS_TTL` | Cache TTL in seconds | `86400` (24h) | +| Variable | Description | Default | +| ------------------- | --------------------------------- | ---------------- | +| `CACHE_TYPE` | Cache backend: `local` or `redis` | `local` | +| `GOMODEL_CACHE_DIR` | Directory for local cache files | `.cache` | +| `REDIS_URL` | Redis connection URL | _(empty)_ | +| `REDIS_KEY` | Redis key for model cache | `gomodel:models` | +| `REDIS_TTL` | Cache TTL in seconds | `86400` (24h) | #### Storage Storage is shared by audit logging, usage tracking, and future features like IAM. -| Variable | Description | Default | -|----------|-------------|---------| -| `STORAGE_TYPE` | Backend: `sqlite`, `postgresql`, or `mongodb` | `sqlite` | -| `SQLITE_PATH` | SQLite database file path | `data/gomodel.db` | -| `POSTGRES_URL` | PostgreSQL connection string | _(empty)_ | -| `POSTGRES_MAX_CONNS` | PostgreSQL connection pool size | `10` | -| `MONGODB_URL` | MongoDB connection string | _(empty)_ | -| `MONGODB_DATABASE` | MongoDB database name | `gomodel` | +| Variable | Description | Default | +| -------------------- | --------------------------------------------- | ----------------- | +| `STORAGE_TYPE` | Backend: `sqlite`, `postgresql`, or `mongodb` | `sqlite` | +| `SQLITE_PATH` | SQLite database file path | `data/gomodel.db` | +| `POSTGRES_URL` | PostgreSQL connection string | _(empty)_ | +| `POSTGRES_MAX_CONNS` | PostgreSQL connection pool size | `10` | +| `MONGODB_URL` | MongoDB connection string | _(empty)_ | +| `MONGODB_DATABASE` | MongoDB database name | `gomodel` | #### Audit Logging -| Variable | Description | Default | -|----------|-------------|---------| -| `LOGGING_ENABLED` | Enable audit logging | `false` | -| `LOGGING_LOG_BODIES` | Log request/response bodies | `true` | -| `LOGGING_LOG_HEADERS` | Log headers (sensitive ones auto-redacted) | `true` | -| `LOGGING_ONLY_MODEL_INTERACTIONS` | Only log AI model endpoints | `true` | -| `LOGGING_BUFFER_SIZE` | In-memory buffer before flush | `1000` | -| `LOGGING_FLUSH_INTERVAL` | Flush interval in seconds | `5` | -| `LOGGING_RETENTION_DAYS` | Auto-delete after N days (0 = forever) | `30` | +| Variable | Description | Default | +| --------------------------------- | ------------------------------------------ | ------- | +| `LOGGING_ENABLED` | Enable audit logging | `false` | +| `LOGGING_LOG_BODIES` | Log request/response bodies | `true` | +| `LOGGING_LOG_HEADERS` | Log headers (sensitive ones auto-redacted) | `true` | +| `LOGGING_ONLY_MODEL_INTERACTIONS` | Only log AI model endpoints | `true` | +| `LOGGING_BUFFER_SIZE` | In-memory buffer before flush | `1000` | +| `LOGGING_FLUSH_INTERVAL` | Flush interval in seconds | `5` | +| `LOGGING_RETENTION_DAYS` | Auto-delete after N days (0 = forever) | `30` | - When `LOGGING_LOG_BODIES` is enabled, request and response bodies are stored in full. These may contain sensitive data such as PII or API keys embedded in prompts. + When `LOGGING_LOG_BODIES` is enabled, request and response bodies are stored + in full. These may contain sensitive data such as PII or API keys embedded in + prompts. #### Token Usage Tracking -| Variable | Description | Default | -|----------|-------------|---------| -| `USAGE_ENABLED` | Enable token usage tracking | `true` | -| `ENFORCE_RETURNING_USAGE_DATA` | Auto-add `include_usage` to streaming requests | `true` | -| `USAGE_BUFFER_SIZE` | In-memory buffer before flush | `1000` | -| `USAGE_FLUSH_INTERVAL` | Flush interval in seconds | `5` | -| `USAGE_RETENTION_DAYS` | Auto-delete after N days (0 = forever) | `90` | +| Variable | Description | Default | +| ------------------------------ | ---------------------------------------------- | ------- | +| `USAGE_ENABLED` | Enable token usage tracking | `true` | +| `ENFORCE_RETURNING_USAGE_DATA` | Auto-add `include_usage` to streaming requests | `true` | +| `USAGE_BUFFER_SIZE` | In-memory buffer before flush | `1000` | +| `USAGE_FLUSH_INTERVAL` | Flush interval in seconds | `5` | +| `USAGE_RETENTION_DAYS` | Auto-delete after N days (0 = forever) | `90` | #### Metrics -| Variable | Description | Default | -|----------|-------------|---------| -| `METRICS_ENABLED` | Enable Prometheus metrics | `false` | -| `METRICS_ENDPOINT` | HTTP path for metrics | `/metrics` | +| Variable | Description | Default | +| ------------------ | ------------------------- | ---------- | +| `METRICS_ENABLED` | Enable Prometheus metrics | `false` | +| `METRICS_ENDPOINT` | HTTP path for metrics | `/metrics` | #### HTTP Client These control timeouts for upstream API requests to LLM providers. -| Variable | Description | Default | -|----------|-------------|---------| -| `HTTP_TIMEOUT` | Overall request timeout in seconds | `600` (10 min) | +| Variable | Description | Default | +| ------------------------------ | -------------------------------------------- | -------------- | +| `HTTP_TIMEOUT` | Overall request timeout in seconds | `600` (10 min) | | `HTTP_RESPONSE_HEADER_TIMEOUT` | Time to wait for response headers in seconds | `600` (10 min) | #### Provider API Keys Set these to automatically register providers. No YAML configuration required. -| Variable | Provider | -|----------|----------| -| `OPENAI_API_KEY` | OpenAI | -| `ANTHROPIC_API_KEY` | Anthropic | -| `GEMINI_API_KEY` | Google Gemini | -| `XAI_API_KEY` | xAI (Grok) | -| `GROQ_API_KEY` | Groq | -| `OLLAMA_BASE_URL` | Ollama (no API key needed) | +| Variable | Provider | +| ------------------- | -------------------------- | +| `OPENAI_API_KEY` | OpenAI | +| `ANTHROPIC_API_KEY` | Anthropic | +| `GEMINI_API_KEY` | Google Gemini | +| `XAI_API_KEY` | xAI (Grok) | +| `GROQ_API_KEY` | Groq | +| `OLLAMA_BASE_URL` | Ollama (no API key needed) | You can also set a custom base URL for any provider using `_BASE_URL` (e.g., `OPENAI_BASE_URL`). @@ -143,7 +139,8 @@ cp .env.template .env ``` - Real environment variables always override values from the `.env` file. The `.env` file is only loaded if it exists — missing it is not an error. + Real environment variables always override values from the `.env` file. The + `.env` file is only loaded if it exists — missing it is not an error. ### 3. Configuration File (YAML) @@ -200,7 +197,9 @@ providers: ``` - The YAML file is entirely optional. Any setting you can put in YAML can also be set via environment variables. Use YAML when you need to configure custom providers or prefer a structured config file. + The YAML file is entirely optional. Any setting you can put in YAML can also + be set via environment variables. Use YAML when you need to configure custom + providers or prefer a structured config file. ## Provider Configuration @@ -260,34 +259,6 @@ providers: ``` - Providers with missing or unresolved API keys are automatically filtered out at startup. Ollama is the only exception — it only requires a base URL. + Providers with missing or unresolved API keys are automatically filtered out + at startup. Ollama is the only exception — it only requires a base URL. - -## Precedence Rules - -When the same setting is defined in multiple places, the highest-priority source wins: - -``` -Environment Variable > YAML File > Code Default -``` - -**Example:** If `config.yaml` sets `port: "3000"` but the environment has `PORT=9090`, the server starts on port **9090**. - -For providers, environment variables override YAML values for the same provider name. If `OPENAI_API_KEY` is set in the environment and also defined in YAML, the environment value is used. - -## Default Values - -All settings have sensible defaults. GOModel starts with zero configuration using these values: - -| Setting | Default | -|---------|---------| -| Server port | `8080` | -| Cache type | `local` | -| Cache directory | `.cache` | -| Storage type | `sqlite` | -| SQLite path | `data/gomodel.db` | -| Audit logging | Disabled | -| Usage tracking | Enabled | -| Metrics | Disabled | -| HTTP timeout | 600s (10 minutes) | -| Providers | None (auto-discovered from env) | diff --git a/docs/docs.json b/docs/docs.json index 8294cbda..bcc09f31 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1,7 +1,7 @@ { "$schema": "https://mintlify.com/docs.json", "theme": "maple", - "name": "GoModel documentation", + "name": "GoModel", "colors": { "primary": "#755c3d" }, From 0ff580c03e17e68e0fa683e2812489db0b2e36ee Mon Sep 17 00:00:00 2001 From: "Jakub A. W" Date: Mon, 9 Feb 2026 20:36:28 +0100 Subject: [PATCH 4/6] docs: fix grammar in configuration page Co-Authored-By: Claude Opus 4.6 --- docs/advanced/configuration.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/advanced/configuration.mdx b/docs/advanced/configuration.mdx index b6439505..5c85428d 100644 --- a/docs/advanced/configuration.mdx +++ b/docs/advanced/configuration.mdx @@ -5,7 +5,7 @@ description: "How to configure GOModel using environment variables, .env files, ## Good defaults -GOModel uses good defaults philosophy. It means that default settings should be enough to use it. +GOModel uses a good defaults philosophy. This means that the default settings should be enough to use it. ## How to override the default settings? @@ -16,7 +16,7 @@ Environment Variables (always win) → config.yaml (optional structured config ``` - As GOModel works out of the box with no configuration files you can try it in + As GOModel works out of the box with no configuration files, you can try it in a minute. TODO: the link to the quick start should be provided here! From fb88feca465d27f9f0ac91d7cbf5005fa5baf960 Mon Sep 17 00:00:00 2001 From: "Jakub A. W" Date: Mon, 9 Feb 2026 20:48:07 +0100 Subject: [PATCH 5/6] refactor: replace hardcoded SQLite path with storage.DefaultSQLitePath constant Introduce a shared DefaultSQLitePath constant in the storage package and use it across all default-path fallbacks so the default SQLite database path is defined in a single place. Co-Authored-By: Claude Opus 4.6 --- config/config.go | 25 +++++++++++++++++-------- internal/auditlog/factory.go | 2 +- internal/storage/sqlite.go | 2 +- internal/storage/storage.go | 5 ++++- internal/usage/factory.go | 2 +- 5 files changed, 24 insertions(+), 12 deletions(-) diff --git a/config/config.go b/config/config.go index 5b092ece..59d57d1a 100644 --- a/config/config.go +++ b/config/config.go @@ -11,6 +11,8 @@ import ( "github.com/joho/godotenv" "gopkg.in/yaml.v3" + + "gomodel/internal/storage" ) // Body size limit constants @@ -207,7 +209,7 @@ func defaultConfig() Config { Storage: StorageConfig{ Type: "sqlite", SQLite: SQLiteStorageConfig{ - Path: "data/gomodel.db", + Path: storage.DefaultSQLitePath, }, PostgreSQL: PostgreSQLStorageConfig{ MaxConns: 10, @@ -260,7 +262,9 @@ func Load() (*Config, error) { } // 4. Env vars always win - applyEnvOverrides(&cfg) + if err := applyEnvOverrides(&cfg); err != nil { + return nil, err + } // 5. Discover providers from env applyProviderEnvVars(&cfg) @@ -318,11 +322,11 @@ func applyYAML(cfg *Config) error { // applyEnvOverrides walks cfg's struct fields and applies env var overrides // based on `env` struct tags. Maps are skipped (providers are handled separately). -func applyEnvOverrides(cfg *Config) { - applyEnvOverridesValue(reflect.ValueOf(cfg).Elem()) +func applyEnvOverrides(cfg *Config) error { + return applyEnvOverridesValue(reflect.ValueOf(cfg).Elem()) } -func applyEnvOverridesValue(v reflect.Value) { +func applyEnvOverridesValue(v reflect.Value) error { t := v.Type() for i := 0; i < t.NumField(); i++ { field := t.Field(i) @@ -334,7 +338,9 @@ func applyEnvOverridesValue(v reflect.Value) { } // Recurse into nested structs if field.Type.Kind() == reflect.Struct { - applyEnvOverridesValue(fieldVal) + if err := applyEnvOverridesValue(fieldVal); err != nil { + return err + } continue } @@ -353,11 +359,14 @@ func applyEnvOverridesValue(v reflect.Value) { case reflect.Bool: fieldVal.SetBool(parseBool(envVal)) case reflect.Int: - if n, err := strconv.Atoi(envVal); err == nil { - fieldVal.SetInt(int64(n)) + n, err := strconv.Atoi(envVal) + if err != nil { + return fmt.Errorf("invalid value for %s (%s): %q is not a valid integer", field.Name, envKey, envVal) } + fieldVal.SetInt(int64(n)) } } + return nil } // knownProvider describes a provider that can be auto-discovered from environment variables. diff --git a/internal/auditlog/factory.go b/internal/auditlog/factory.go index 6a3f9635..f9ef6609 100644 --- a/internal/auditlog/factory.go +++ b/internal/auditlog/factory.go @@ -101,7 +101,7 @@ func buildStorageConfig(cfg *config.Config) storage.Config { storageCfg.Type = storage.TypeSQLite } if storageCfg.SQLite.Path == "" { - storageCfg.SQLite.Path = "data/gomodel.db" + storageCfg.SQLite.Path = storage.DefaultSQLitePath } if storageCfg.MongoDB.Database == "" { storageCfg.MongoDB.Database = "gomodel" diff --git a/internal/storage/sqlite.go b/internal/storage/sqlite.go index 52c6579d..5b84ffff 100644 --- a/internal/storage/sqlite.go +++ b/internal/storage/sqlite.go @@ -18,7 +18,7 @@ type sqliteStorage struct { // It enables WAL mode for better concurrent read/write performance. func NewSQLite(cfg SQLiteConfig) (Storage, error) { if cfg.Path == "" { - cfg.Path = "data/gomodel.db" + cfg.Path = DefaultSQLitePath } // Ensure directory exists diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 4a1775c3..8e0230e2 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -16,6 +16,9 @@ const ( TypeMongoDB = "mongodb" ) +// DefaultSQLitePath is the default file path for the SQLite database. +const DefaultSQLitePath = "data/gomodel.db" + // Config holds storage configuration type Config struct { // Type specifies the storage backend: "sqlite", "postgresql", or "mongodb" @@ -97,7 +100,7 @@ func DefaultConfig() Config { return Config{ Type: TypeSQLite, SQLite: SQLiteConfig{ - Path: "data/gomodel.db", + Path: DefaultSQLitePath, }, PostgreSQL: PostgreSQLConfig{ MaxConns: 10, diff --git a/internal/usage/factory.go b/internal/usage/factory.go index d677569d..5b28097a 100644 --- a/internal/usage/factory.go +++ b/internal/usage/factory.go @@ -132,7 +132,7 @@ func buildStorageConfig(cfg *config.Config) storage.Config { storageCfg.Type = storage.TypeSQLite } if storageCfg.SQLite.Path == "" { - storageCfg.SQLite.Path = "data/gomodel.db" + storageCfg.SQLite.Path = storage.DefaultSQLitePath } if storageCfg.MongoDB.Database == "" { storageCfg.MongoDB.Database = "gomodel" From 47fb95b82ba250b04616ab54a77e57f853976d4c Mon Sep 17 00:00:00 2001 From: "Jakub A. W" Date: Mon, 9 Feb 2026 20:59:37 +0100 Subject: [PATCH 6/6] fix: check error return from applyEnvOverrides in test Co-Authored-By: Claude Opus 4.6 --- config/config_helpers_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/config_helpers_test.go b/config/config_helpers_test.go index f11213a5..ecb0f8a7 100644 --- a/config/config_helpers_test.go +++ b/config/config_helpers_test.go @@ -3,6 +3,8 @@ package config import ( "os" "testing" + + "github.com/stretchr/testify/require" ) // TestExpandString tests the expandString function with various scenarios @@ -492,7 +494,7 @@ func TestApplyEnvOverrides(t *testing.T) { } cfg := defaultConfig() - applyEnvOverrides(&cfg) + require.NoError(t, applyEnvOverrides(&cfg)) tt.check(t, &cfg) }) }