feat: refactor config system to unified 3-layer pipeline and add conf…#60
feat: refactor config system to unified 3-layer pipeline and add conf…#60SantiagoDePolonia merged 3 commits intomainfrom
Conversation
…iguration docs Replace Viper with direct yaml.v3 + godotenv for a simpler, predictable config pipeline: code defaults → optional config.yaml overlay → env vars (always win). This removes ~10 transitive dependencies and eliminates the dual code path that existed depending on whether config.yaml was present. Key changes: - Single defaultConfig() as source of truth for all defaults - Declarative env var mapping tables (applyEnvVars) - Table-driven provider auto-discovery from env vars (applyProviderEnvVars) - New HTTPConfig and CacheDir fields in Config struct - config.yaml renamed to config.example.yaml (fully commented template) - Zero-config startup: .env.template values all commented out - Add Mintlify configuration docs page (docs/advanced/configuration.mdx) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. 📝 WalkthroughWalkthroughReplaces Viper-based config with a three-layer pipeline: in-code defaults → optional YAML overlay → environment-variable overrides. Adds YAML-tagged Config schema, provider auto-discovery from env vars, helpers for YAML/env application, tests and docs updates, .env/.gitignore/template tweaks, and dependency cleanup (yaml.v3). Changes
Sequence Diagram(s)sequenceDiagram
participant App as Application
participant Loader as ConfigLoader
participant FS as FileSystem (config.yaml)
participant Env as Environment
participant Providers as ProviderDiscovery
App->>Loader: Load()
Loader->>Loader: defaultConfig()
Loader->>FS: read config/config.yaml or ./config.yaml
alt YAML present
FS-->>Loader: YAML blob
Loader->>Loader: applyYAML(cfg, blob)
else no YAML
FS-->>Loader: not found
end
Loader->>Env: read env vars (strings/bools/ints)
Env-->>Loader: env values
Loader->>Loader: applyEnvVars(cfg)
Loader->>Providers: discover known provider env vars
Providers-->>Loader: provider entries merged into cfg.Providers
Loader->>Loader: removeEmptyProviders(cfg)
Loader-->>App: Config (final)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/httpclient/client.go (1)
66-77: 🧹 Nitpick | 🔵 TrivialDual env-var read path for HTTP timeouts — consider consolidating.
DefaultConfig()readsHTTP_TIMEOUTandHTTP_RESPONSE_HEADER_TIMEOUTdirectly from the environment, whileconfig.HTTPConfigalso maps these same env vars viaapplyEnvVars. The loaded config values are never passed toNewHTTPClient; providers instead callNewDefaultHTTPClient(), which triggers a fresh environment read. Ifconfig.HTTPConfigis intended as the configuration source of truth, consider accepting it in the HTTP client factory to avoid maintaining two separate parsing paths (note: they differ in behavior—DefaultConfig()supports duration format like "2m", while config only accepts integers).
🤖 Fix all issues with AI agents
In `@config/config_helpers_test.go`:
- Around line 559-589: In TestIntegration_ExpandAndFilter replace the manual
os.Setenv("OPENAI_API_KEY", ...) and deferred os.Unsetenv call with
t.Setenv("OPENAI_API_KEY", "sk-openai-123") to ensure the environment variable
is automatically restored on test exit; update the setup in the
TestIntegration_ExpandAndFilter t.Run block so it uses t.Setenv and remove the
os.Unsetenv defer, keeping the rest of the test logic (cfg := defaultConfig(),
cfg.Providers setup, removeEmptyProviders(&cfg) and assertions) unchanged.
- Around line 549-556: The test "skips when no env vars set" can fail if
provider environment variables are present; update the subtest to clear provider
env vars before running by calling the existing clearProviderEnvVars(t) helper
(or implement an equivalent local cleanup) prior to creating cfg and calling
applyProviderEnvVars; reference the test helpers and functions
applyProviderEnvVars and defaultConfig to locate where to add the call, and
ensure any teardown/restoration of env vars is handled so other tests are
unaffected.
In `@config/config_test.go`:
- Around line 48-61: The helper withTempDir mutates the process-wide working
directory (via os.Chdir) which is not safe for tests run with t.Parallel and
also relies on t.Cleanup LIFO behavior when called multiple times (e.g.,
TestLoad_HTTPConfig); update the function comment to explicitly warn about these
constraints—add a note like “NOTE: Not safe for parallel tests — changes
process-wide cwd; relies on t.Cleanup LIFO ordering when called multiple times”
next to the withTempDir declaration so future contributors see the limitation.
- Around line 9-46: Add a one-line clarifying comment above the t.Setenv +
os.Unsetenv calls in clearProviderEnvVars and clearAllConfigEnvVars explaining
the pattern: use t.Setenv to register test cleanup/restoration and then
os.Unsetenv to remove the env var for the test run (so it’s truly absent, not
just empty); place the comment just before the loop or immediately above the
t.Setenv call and reference the use of t.Setenv and os.Unsetenv in the comment
for clarity.
In `@config/config.example.yaml`:
- Around line 30-37: The logging defaults are inconsistent: update the
configuration so the YAML keys logging.log_bodies and logging.log_headers match
the environment variables LOGGING_LOG_BODIES and LOGGING_LOG_HEADERS in
.env.template; choose the intended default (true or false), set that value
consistently in config.example.yaml and .env.template, and update any related
documentation/comments to reflect the chosen default so users see the same
recommended setting across files.
In `@config/config.go`:
- Around line 466-494: Update the comment for expandString to explicitly state
that environment variables set to the empty string are treated as unset:
`${VAR}` will remain literal `${VAR}` and `${VAR:-default}` will use the default
value (matching bash `:-` semantics), and mention this is intentional and may
surprise users who explicitly set an empty value; place the doc comment
immediately above the expandString function (reference: expandString) so callers
and maintainers see the behavior (you can also add a short note next to
parseBool if desired for consistency).
- Around line 416-450: The Ollama-specific guard inside applyProviderEnvVars
(the if checking kp.providerType == "ollama" && apiKey == "" && baseURL == "")
is unreachable because the earlier continue already skips when apiKey == "" &&
baseURL == ""; remove that dead branch from applyProviderEnvVars (referencing
knownProviders and kp.providerType == "ollama") so the function simply uses the
existing generic env-var handling and provider override/add logic without the
redundant special-case check.
- Around line 249-252: In Load(), don’t discard the error from godotenv.Load():
capture err := godotenv.Load() and if err != nil then allow the "file not found"
case to be ignored (errors.Is(err, fs.ErrNotExist) or errors.Is(err,
os.ErrNotExist)) but return the error (or wrap it with context) for any other
parse/read error so syntax problems in the .env surface; update the Load()
function to use that check around godotenv.Load() and return the wrapped error
from Load() when appropriate.
In `@docs/advanced/configuration.mdx`:
- Around line 164-188: Add a security notice near the YAML example warning users
not to commit secrets like master_key and api_key (or config/config.yaml) into
source control and show a safe alternative using environment variable expansion
(e.g. ${ENV_VAR}) instead of plaintext; add a <Warning> block immediately above
or below the code sample that explicitly calls out master_key and api_key and
demonstrates replacing them with ${OPENAI_API_KEY} / ${MASTER_KEY} placeholders
and a brief line about keeping actual config files out of git.
- Around line 42-46: Update the docs table to match the actual default for
BODY_SIZE_LIMIT: change the Default column for `BODY_SIZE_LIMIT` from `_(no
limit)_` to `10M` to reflect the value set in config (DefaultBodySizeLimit = 10
* 1024 * 1024 in config/config.go) and the .env.template default; locate the
`BODY_SIZE_LIMIT` row in docs/advanced/configuration.mdx and replace the default
text accordingly.
| t.Run("skips when no env vars set", func(t *testing.T) { | ||
| cfg := defaultConfig() | ||
| applyProviderEnvVars(&cfg) | ||
|
|
||
| if len(cfg.Providers) != 0 { | ||
| t.Errorf("expected no providers, got %d", len(cfg.Providers)) | ||
| } | ||
| }) |
There was a problem hiding this comment.
Test may fail if provider env vars are set in the ambient environment.
Unlike the tests in config_test.go that call clearProviderEnvVars(t), this subtest assumes no provider env vars are set. If OPENAI_API_KEY (or any other provider key) is present in the CI/developer shell, this assertion will fail.
Proposed fix
t.Run("skips when no env vars set", func(t *testing.T) {
+ for _, kp := range knownProviders {
+ t.Setenv(kp.apiKeyEnv, "")
+ os.Unsetenv(kp.apiKeyEnv)
+ t.Setenv(kp.baseURLEnv, "")
+ os.Unsetenv(kp.baseURLEnv)
+ }
cfg := defaultConfig()
applyProviderEnvVars(&cfg)Or reuse the clearProviderEnvVars(t) helper from config_test.go by moving it to a shared test helper file.
🤖 Prompt for AI Agents
In `@config/config_helpers_test.go` around lines 549 - 556, The test "skips when
no env vars set" can fail if provider environment variables are present; update
the subtest to clear provider env vars before running by calling the existing
clearProviderEnvVars(t) helper (or implement an equivalent local cleanup) prior
to creating cfg and calling applyProviderEnvVars; reference the test helpers and
functions applyProviderEnvVars and defaultConfig to locate where to add the
call, and ensure any teardown/restoration of env vars is handled so other tests
are unaffected.
| // TestIntegration_ExpandAndFilter tests env var expansion in YAML + provider filtering | ||
| func TestIntegration_ExpandAndFilter(t *testing.T) { | ||
| t.Run("expand and filter mixed providers", func(t *testing.T) { | ||
| _ = os.Setenv("OPENAI_API_KEY", "sk-openai-123") | ||
| defer os.Unsetenv("OPENAI_API_KEY") | ||
|
|
||
| cfg := defaultConfig() | ||
| cfg.Providers = map[string]ProviderConfig{ | ||
| "openai": { | ||
| Type: "openai", | ||
| APIKey: "sk-openai-123", // already expanded | ||
| }, | ||
| "openai-fallback": { | ||
| Type: "openai", | ||
| APIKey: "${OPENAI_FALLBACK_KEY}", // unresolved | ||
| }, | ||
| "anthropic": { | ||
| Type: "anthropic", | ||
| APIKey: "${ANTHROPIC_API_KEY}", // unresolved | ||
| }, | ||
| } | ||
| removeEmptyProviders(&cfg) | ||
|
|
||
| if len(cfg.Providers) != 1 { | ||
| t.Errorf("expected 1 provider, got %d: %v", len(cfg.Providers), cfg.Providers) | ||
| } | ||
| if _, ok := cfg.Providers["openai"]; !ok { | ||
| t.Error("expected openai to survive filtering") | ||
| } | ||
| }) | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Prefer t.Setenv over manual os.Setenv/os.Unsetenv for test isolation.
Lines 562-563 use os.Setenv/defer os.Unsetenv while the rest of this file's new tests use t.Setenv. This inconsistency could lead to env leaks if the test panics before the defer runs.
Proposed fix
t.Run("expand and filter mixed providers", func(t *testing.T) {
- _ = os.Setenv("OPENAI_API_KEY", "sk-openai-123")
- defer os.Unsetenv("OPENAI_API_KEY")
+ t.Setenv("OPENAI_API_KEY", "sk-openai-123")🤖 Prompt for AI Agents
In `@config/config_helpers_test.go` around lines 559 - 589, In
TestIntegration_ExpandAndFilter replace the manual os.Setenv("OPENAI_API_KEY",
...) and deferred os.Unsetenv call with t.Setenv("OPENAI_API_KEY",
"sk-openai-123") to ensure the environment variable is automatically restored on
test exit; update the setup in the TestIntegration_ExpandAndFilter t.Run block
so it uses t.Setenv and remove the os.Unsetenv defer, keeping the rest of the
test logic (cfg := defaultConfig(), cfg.Providers setup,
removeEmptyProviders(&cfg) and assertions) unchanged.
| // clearProviderEnvVars unsets all known provider-related environment variables. | ||
| func clearProviderEnvVars(t *testing.T) { | ||
| t.Helper() | ||
| for _, key := range []string{ | ||
| "OPENAI_API_KEY", "OPENAI_BASE_URL", | ||
| "ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL", | ||
| "GEMINI_API_KEY", "GEMINI_BASE_URL", | ||
| "XAI_API_KEY", "XAI_BASE_URL", | ||
| "GROQ_API_KEY", "GROQ_BASE_URL", | ||
| "OLLAMA_API_KEY", "OLLAMA_BASE_URL", | ||
| } { | ||
| t.Setenv(key, "") | ||
| os.Unsetenv(key) | ||
| } | ||
| } | ||
|
|
||
| // Clear any existing environment variables | ||
| _ = os.Unsetenv("PORT") | ||
| _ = os.Unsetenv("OPENAI_API_KEY") | ||
| // clearAllConfigEnvVars unsets all config-related environment variables. | ||
| func clearAllConfigEnvVars(t *testing.T) { | ||
| t.Helper() | ||
| for _, key := range []string{ | ||
| "PORT", "GOMODEL_MASTER_KEY", "BODY_SIZE_LIMIT", | ||
| "CACHE_TYPE", "GOMODEL_CACHE_DIR", | ||
| "REDIS_URL", "REDIS_KEY", "REDIS_TTL", | ||
| "STORAGE_TYPE", "SQLITE_PATH", "POSTGRES_URL", "POSTGRES_MAX_CONNS", | ||
| "MONGODB_URL", "MONGODB_DATABASE", | ||
| "METRICS_ENABLED", "METRICS_ENDPOINT", | ||
| "LOGGING_ENABLED", "LOGGING_LOG_BODIES", "LOGGING_LOG_HEADERS", | ||
| "LOGGING_ONLY_MODEL_INTERACTIONS", "LOGGING_BUFFER_SIZE", | ||
| "LOGGING_FLUSH_INTERVAL", "LOGGING_RETENTION_DAYS", | ||
| "USAGE_ENABLED", "ENFORCE_RETURNING_USAGE_DATA", | ||
| "USAGE_BUFFER_SIZE", "USAGE_FLUSH_INTERVAL", "USAGE_RETENTION_DAYS", | ||
| "HTTP_TIMEOUT", "HTTP_RESPONSE_HEADER_TIMEOUT", | ||
| } { | ||
| t.Setenv(key, "") | ||
| os.Unsetenv(key) | ||
| } | ||
| clearProviderEnvVars(t) | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
t.Setenv + os.Unsetenv combo is correct but deserves a brief comment.
The t.Setenv(key, "") registers the cleanup (to restore the original value), while os.Unsetenv(key) actually removes the variable from the environment for the test duration. This two-step pattern isn't immediately obvious to readers.
Consider adding a one-line comment explaining the intent, e.g., // t.Setenv registers cleanup; os.Unsetenv ensures the var is truly absent (not just empty).
🤖 Prompt for AI Agents
In `@config/config_test.go` around lines 9 - 46, Add a one-line clarifying comment
above the t.Setenv + os.Unsetenv calls in clearProviderEnvVars and
clearAllConfigEnvVars explaining the pattern: use t.Setenv to register test
cleanup/restoration and then os.Unsetenv to remove the env var for the test run
(so it’s truly absent, not just empty); place the comment just before the loop
or immediately above the t.Setenv call and reference the use of t.Setenv and
os.Unsetenv in the comment for clarity.
| // withTempDir runs fn in a temporary directory, restoring the original working directory afterward. | ||
| func withTempDir(t *testing.T, fn func(dir string)) { | ||
| t.Helper() | ||
| tempDir := t.TempDir() | ||
| originalDir, err := os.Getwd() | ||
| if err != nil { | ||
| t.Fatalf("Load() failed: %v", err) | ||
| t.Fatalf("Failed to get current directory: %v", err) | ||
| } | ||
| if err := os.Chdir(tempDir); err != nil { | ||
| t.Fatalf("Failed to change to temp directory: %v", err) | ||
| } | ||
| t.Cleanup(func() { _ = os.Chdir(originalDir) }) | ||
| fn(tempDir) | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
os.Chdir in tests is not parallel-safe — consider documenting this constraint.
The withTempDir helper changes the process-wide working directory. This is fine as long as no test uses t.Parallel(), but future contributors might not realize this. Also, calling withTempDir multiple times in a single test (e.g., TestLoad_HTTPConfig) relies on t.Cleanup LIFO ordering — it works, but is subtle.
Consider adding a comment like // NOTE: Not safe for parallel tests — changes process-wide cwd.
🤖 Prompt for AI Agents
In `@config/config_test.go` around lines 48 - 61, The helper withTempDir mutates
the process-wide working directory (via os.Chdir) which is not safe for tests
run with t.Parallel and also relies on t.Cleanup LIFO behavior when called
multiple times (e.g., TestLoad_HTTPConfig); update the function comment to
explicitly warn about these constraints—add a note like “NOTE: Not safe for
parallel tests — changes process-wide cwd; relies on t.Cleanup LIFO ordering
when called multiple times” next to the withTempDir declaration so future
contributors see the limitation.
config/config.example.yaml
Outdated
| # logging: | ||
| # enabled: false | ||
| # log_bodies: true # WARNING: may contain sensitive data | ||
| # log_headers: true | ||
| # buffer_size: 1000 | ||
| # flush_interval: 5 # seconds | ||
| # retention_days: 30 # 0 = keep forever | ||
| # only_model_interactions: true |
There was a problem hiding this comment.
Inconsistency between config.example.yaml and .env.template for log_bodies / log_headers defaults.
This file shows log_bodies: true and log_headers: true (lines 32-33), matching the documentation defaults. However, .env.template lines 68 and 72 suggest LOGGING_LOG_BODIES=false and LOGGING_LOG_HEADERS=false. Users referencing different files will get different impressions of the default/recommended value.
Consider aligning these so users aren't confused about whether body/header logging is on or off by default.
🤖 Prompt for AI Agents
In `@config/config.example.yaml` around lines 30 - 37, The logging defaults are
inconsistent: update the configuration so the YAML keys logging.log_bodies and
logging.log_headers match the environment variables LOGGING_LOG_BODIES and
LOGGING_LOG_HEADERS in .env.template; choose the intended default (true or
false), set that value consistently in config.example.yaml and .env.template,
and update any related documentation/comments to reflect the chosen default so
users see the same recommended setting across files.
| func Load() (*Config, error) { | ||
| // 1. Load .env into process env (ignore if not found) | ||
| _ = godotenv.Load() | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
godotenv.Load error handling missing file os.IsNotExist
💡 Result:
godotenv.Load() returns an error (it tries to open/read the file(s) you pass; with no args it looks for .env in the current working directory). (pkg.go.dev)
So if .env is optional (common in prod), ignore only the “file not found” case:
import (
"errors"
"os"
"github.com/joho/godotenv"
)
func loadDotEnvOptional() error {
if err := godotenv.Load(); err != nil {
if errors.Is(err, os.ErrNotExist) || os.IsNotExist(err) {
return nil // .env missing: OK
}
return err // other I/O/parse errors: not OK
}
return nil
}Why os.IsNotExist works: godotenv.Load ultimately reads the file and propagates the underlying file-open/read error (i.e., the usual os “not exist” error). (darjun.github.io)
Citations:
- 1: https://pkg.go.dev/github.com/joho/godotenv
- 2: https://darjun.github.io/2020/02/12/godailylib/godotenv/?utm_source=openai
🏁 Script executed:
# Check the current code in config/config.go around the mentioned lines
find . -name "config.go" -type f | head -20Repository: ENTERPILOT/GOModel
Length of output: 80
🏁 Script executed:
# View the code at lines 249-252 in config/config.go
sed -n '245,260p' ./config/config.goRepository: ENTERPILOT/GOModel
Length of output: 487
Silently discarding .env parse errors may hide misconfigurations.
godotenv.Load() returns an error when the .env file is missing (acceptable) and when it exists but has syntax errors (should be surfaced). The current code discards all errors. Consider distinguishing these cases:
Proposed fix
- _ = godotenv.Load()
+ if err := godotenv.Load(); err != nil && !os.IsNotExist(err) {
+ return nil, fmt.Errorf("failed to parse .env file: %w", err)
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| func Load() (*Config, error) { | |
| // 1. Load .env into process env (ignore if not found) | |
| _ = godotenv.Load() | |
| func Load() (*Config, error) { | |
| // 1. Load .env into process env (ignore if not found) | |
| if err := godotenv.Load(); err != nil && !os.IsNotExist(err) { | |
| return nil, fmt.Errorf("failed to parse .env file: %w", err) | |
| } |
🤖 Prompt for AI Agents
In `@config/config.go` around lines 249 - 252, In Load(), don’t discard the error
from godotenv.Load(): capture err := godotenv.Load() and if err != nil then
allow the "file not found" case to be ignored (errors.Is(err, fs.ErrNotExist) or
errors.Is(err, os.ErrNotExist)) but return the error (or wrap it with context)
for any other parse/read error so syntax problems in the .env surface; update
the Load() function to use that check around godotenv.Load() and return the
wrapped error from Load() when appropriate.
| func applyProviderEnvVars(cfg *Config) { | ||
| for _, kp := range knownProviders { | ||
| apiKey := os.Getenv(kp.apiKeyEnv) | ||
| baseURL := os.Getenv(kp.baseURLEnv) | ||
|
|
||
| // Skip if no env vars set for this provider | ||
| if apiKey == "" && baseURL == "" { | ||
| continue | ||
| } | ||
|
|
||
| // Ollama special case: no API key required, enabled via base URL | ||
| if kp.providerType == "ollama" && apiKey == "" && baseURL == "" { | ||
| continue | ||
| } | ||
|
|
||
| existing, exists := cfg.Providers[kp.name] | ||
| if exists { | ||
| // Override existing provider's env-sourced values | ||
| if apiKey != "" { | ||
| existing.APIKey = apiKey | ||
| } | ||
| if baseURL != "" { | ||
| existing.BaseURL = baseURL | ||
| } | ||
| cfg.Providers[kp.name] = existing | ||
| } else { | ||
| // Add new provider from env | ||
| cfg.Providers[kp.name] = ProviderConfig{ | ||
| Type: kp.providerType, | ||
| APIKey: apiKey, | ||
| BaseURL: baseURL, | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Dead code: Ollama special-case check on lines 427-429 is unreachable.
The guard on line 422 (apiKey == "" && baseURL == "") already causes a continue for all providers when both are empty. By line 427, at least one of apiKey or baseURL is non-empty, so the condition apiKey == "" && baseURL == "" can never be true.
Proposed fix — remove dead code
// Skip if no env vars set for this provider
if apiKey == "" && baseURL == "" {
continue
}
- // Ollama special case: no API key required, enabled via base URL
- if kp.providerType == "ollama" && apiKey == "" && baseURL == "" {
- continue
- }
-
existing, exists := cfg.Providers[kp.name]📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| func applyProviderEnvVars(cfg *Config) { | |
| for _, kp := range knownProviders { | |
| apiKey := os.Getenv(kp.apiKeyEnv) | |
| baseURL := os.Getenv(kp.baseURLEnv) | |
| // Skip if no env vars set for this provider | |
| if apiKey == "" && baseURL == "" { | |
| continue | |
| } | |
| // Ollama special case: no API key required, enabled via base URL | |
| if kp.providerType == "ollama" && apiKey == "" && baseURL == "" { | |
| continue | |
| } | |
| existing, exists := cfg.Providers[kp.name] | |
| if exists { | |
| // Override existing provider's env-sourced values | |
| if apiKey != "" { | |
| existing.APIKey = apiKey | |
| } | |
| if baseURL != "" { | |
| existing.BaseURL = baseURL | |
| } | |
| cfg.Providers[kp.name] = existing | |
| } else { | |
| // Add new provider from env | |
| cfg.Providers[kp.name] = ProviderConfig{ | |
| Type: kp.providerType, | |
| APIKey: apiKey, | |
| BaseURL: baseURL, | |
| } | |
| } | |
| } | |
| } | |
| func applyProviderEnvVars(cfg *Config) { | |
| for _, kp := range knownProviders { | |
| apiKey := os.Getenv(kp.apiKeyEnv) | |
| baseURL := os.Getenv(kp.baseURLEnv) | |
| // Skip if no env vars set for this provider | |
| if apiKey == "" && baseURL == "" { | |
| continue | |
| } | |
| existing, exists := cfg.Providers[kp.name] | |
| if exists { | |
| // Override existing provider's env-sourced values | |
| if apiKey != "" { | |
| existing.APIKey = apiKey | |
| } | |
| if baseURL != "" { | |
| existing.BaseURL = baseURL | |
| } | |
| cfg.Providers[kp.name] = existing | |
| } else { | |
| // Add new provider from env | |
| cfg.Providers[kp.name] = ProviderConfig{ | |
| Type: kp.providerType, | |
| APIKey: apiKey, | |
| BaseURL: baseURL, | |
| } | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@config/config.go` around lines 416 - 450, The Ollama-specific guard inside
applyProviderEnvVars (the if checking kp.providerType == "ollama" && apiKey ==
"" && baseURL == "") is unreachable because the earlier continue already skips
when apiKey == "" && baseURL == ""; remove that dead branch from
applyProviderEnvVars (referencing knownProviders and kp.providerType ==
"ollama") so the function simply uses the existing generic env-var handling and
provider override/add logic without the redundant special-case check.
| // expandString expands environment variable references like ${VAR} or ${VAR:-default} in a string. | ||
| func expandString(s string) string { | ||
| if s == "" { | ||
| return s | ||
| } | ||
| return os.Expand(s, func(key string) string { | ||
| // Check for default value syntax ${VAR:-default} | ||
| varname := key | ||
| defaultValue := "" | ||
| hasDefault := false | ||
| if strings.Contains(key, ":-") { | ||
| parts := strings.SplitN(key, ":-", 2) | ||
| varname = parts[0] | ||
| defaultValue = parts[1] | ||
| if idx := strings.Index(key, ":-"); idx >= 0 { | ||
| varname = key[:idx] | ||
| defaultValue = key[idx+2:] | ||
| hasDefault = true | ||
| } | ||
|
|
||
| // Try to get from environment | ||
| value := os.Getenv(varname) | ||
| if value == "" { | ||
| // If default syntax was used (even with empty default), return the default | ||
| if hasDefault { | ||
| return defaultValue | ||
| } | ||
| // If not in environment and no default syntax, return the original placeholder | ||
| // This allows config to work with or without env vars | ||
| return "${" + key + "}" | ||
| } | ||
| return value | ||
| }) | ||
| } | ||
|
|
||
| // removeEmptyProviders removes providers with empty API keys | ||
| func removeEmptyProviders(cfg Config) Config { | ||
| filteredProviders := make(map[string]ProviderConfig) | ||
| for name, pCfg := range cfg.Providers { | ||
| // Preserve Ollama providers with a non-empty BaseURL (no API key required) | ||
| if pCfg.Type == "ollama" && pCfg.BaseURL != "" { | ||
| filteredProviders[name] = pCfg | ||
| continue | ||
| } | ||
| // Keep provider only if API key doesn't contain unexpanded placeholders | ||
| if pCfg.APIKey != "" && !strings.Contains(pCfg.APIKey, "${") { | ||
| filteredProviders[name] = pCfg | ||
| } | ||
| } | ||
| cfg.Providers = filteredProviders | ||
| return cfg | ||
| } | ||
|
|
||
| // getEnvOrDefault returns the environment variable value or the default if not set | ||
| func getEnvOrDefault(key, defaultValue string) string { | ||
| if value := os.Getenv(key); value != "" { | ||
| return value | ||
| } | ||
| return defaultValue | ||
| } | ||
|
|
||
| // getEnvIntOrDefault returns the environment variable as int or the default if not set/invalid | ||
| func getEnvIntOrDefault(key string, defaultValue int) int { | ||
| if value := os.Getenv(key); value != "" { | ||
| if intVal, err := strconv.Atoi(value); err == nil { | ||
| return intVal | ||
| } | ||
| } | ||
| return defaultValue | ||
| } | ||
|
|
||
| // getEnvBool returns true if the environment variable is "true" or "1" | ||
| func getEnvBool(key string) bool { | ||
| value := os.Getenv(key) | ||
| return strings.EqualFold(value, "true") || value == "1" | ||
| } | ||
|
|
||
| // getEnvBoolOrDefault returns the boolean value of the environment variable, | ||
| // or the default value if the variable is not set | ||
| func getEnvBoolOrDefault(key string, defaultValue bool) bool { | ||
| value := os.Getenv(key) | ||
| if value == "" { | ||
| return defaultValue | ||
| } | ||
| return strings.EqualFold(value, "true") || value == "1" | ||
| // parseBool returns true if s is "true" or "1" (case-insensitive). | ||
| func parseBool(s string) bool { | ||
| return strings.EqualFold(s, "true") || s == "1" | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
expandString treats empty env vars as unset — this is intentional but worth documenting.
An env var set to "" will not expand ${VAR} (it stays as the literal ${VAR}), and ${VAR:-default} will use the default. This matches bash :- semantics but may surprise users who explicitly set a var to empty. A brief doc comment noting this behavior would help.
🤖 Prompt for AI Agents
In `@config/config.go` around lines 466 - 494, Update the comment for expandString
to explicitly state that environment variables set to the empty string are
treated as unset: `${VAR}` will remain literal `${VAR}` and `${VAR:-default}`
will use the default value (matching bash `:-` semantics), and mention this is
intentional and may surprise users who explicitly set an empty value; place the
doc comment immediately above the expandString function (reference:
expandString) so callers and maintainers see the behavior (you can also add a
short note next to parseBool if desired for consistency).
| | 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)_ | |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find BodySizeLimit/body_size_limit definition in config/config.go
rg -n 'BodySizeLimit|body_size_limit' --type=go config/Repository: ENTERPILOT/GOModel
Length of output: 1291
🏁 Script executed:
#!/bin/bash
# Check .env.template for BODY_SIZE_LIMIT
cat -n .env.template | head -20Repository: ENTERPILOT/GOModel
Length of output: 917
🏁 Script executed:
#!/bin/bash
# Check the documentation file around lines 42-46
cat -n docs/advanced/configuration.mdx | sed -n '40,50p'Repository: ENTERPILOT/GOModel
Length of output: 503
BODY_SIZE_LIMIT default in docs conflicts with actual code.
The docs state the default is _(no limit)_, but config/config.go (line 17) defines DefaultBodySizeLimit = 10 * 1024 * 1024 (10MB), which is also documented in .env.template (line 5). Update the table to show 10M as the default.
🤖 Prompt for AI Agents
In `@docs/advanced/configuration.mdx` around lines 42 - 46, Update the docs table
to match the actual default for BODY_SIZE_LIMIT: change the Default column for
`BODY_SIZE_LIMIT` from `_(no limit)_` to `10M` to reflect the value set in
config (DefaultBodySizeLimit = 10 * 1024 * 1024 in config/config.go) and the
.env.template default; locate the `BODY_SIZE_LIMIT` row in
docs/advanced/configuration.mdx and replace the default text accordingly.
| ```yaml | ||
| server: | ||
| port: "3000" | ||
| master_key: "my-secret-key" | ||
|
|
||
| cache: | ||
| type: redis | ||
| redis: | ||
| url: "redis://my-redis:6379" | ||
|
|
||
| providers: | ||
| openai: | ||
| type: openai | ||
| api_key: "sk-..." | ||
|
|
||
| anthropic: | ||
| type: anthropic | ||
| api_key: "sk-ant-..." | ||
|
|
||
| # Custom OpenAI-compatible provider | ||
| my-custom-llm: | ||
| type: openai | ||
| base_url: "https://api.example.com/v1" | ||
| api_key: "..." | ||
| ``` |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider adding a security note about not committing secrets in YAML.
The YAML examples show master_key and api_key values in plaintext. While config/config.yaml is gitignored, it would be helpful to add a <Warning> advising users to never commit config.yaml with real credentials, or to use ${ENV_VAR} expansion syntax for secrets instead.
🤖 Prompt for AI Agents
In `@docs/advanced/configuration.mdx` around lines 164 - 188, Add a security
notice near the YAML example warning users not to commit secrets like master_key
and api_key (or config/config.yaml) into source control and show a safe
alternative using environment variable expansion (e.g. ${ENV_VAR}) instead of
plaintext; add a <Warning> block immediately above or below the code sample that
explicitly calls out master_key and api_key and demonstrates replacing them with
${OPENAI_API_KEY} / ${MASTER_KEY} placeholders and a brief line about keeping
actual config files out of git.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@config/config.example.yaml`:
- Around line 89-92: Add a consistent blank line separator between the Groq
example comment ("api_key: \"${GROQ_API_KEY}\"") and the next provider header
("# azure-openai:") so the provider blocks match the spacing used elsewhere
(e.g., the blocks around lines with "azure-openai" and the preceding Groq
example should each have one blank line separating them and the block body).
Locate the commented Groq API key line and the "# azure-openai:" header and
adjust spacing so there is one blank line before and after provider headers to
normalize formatting across example blocks.
- Around line 85-101: The examples use shell-style `"${VAR}"` which yaml.v3 will
parse as literal strings and not interpolate environment variables; update the
commented provider examples (the groq, azure-openai, deepseek blocks) to replace
`"${GROQ_API_KEY}"`, `"${AZURE_OPENAI_API_KEY}"`, and `"${DEEPSEEK_API_KEY}"`
with plain placeholder text (e.g. "YOUR_API_KEY") or add a short inline comment
that these are illustrative only and env vars are injected by the code path that
calls applyProviderEnvVars; ensure all three commented examples are changed
consistently.
…iguration docs
Replace Viper with direct yaml.v3 + godotenv for a simpler, predictable config pipeline: code defaults → optional config.yaml overlay → env vars (always win). This removes ~10 transitive dependencies and eliminates the dual code path that existed depending on whether config.yaml was present.
Key changes:
Summary by CodeRabbit
New Features
Chores
Documentation