Skip to content

feat: refactor config system to unified 3-layer pipeline and add conf…#60

Merged
SantiagoDePolonia merged 3 commits intomainfrom
gomodel-config
Feb 9, 2026
Merged

feat: refactor config system to unified 3-layer pipeline and add conf…#60
SantiagoDePolonia merged 3 commits intomainfrom
gomodel-config

Conversation

@SantiagoDePolonia
Copy link
Copy Markdown
Contributor

@SantiagoDePolonia SantiagoDePolonia commented Feb 9, 2026

…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)

Summary by CodeRabbit

  • New Features

    • Three-layer configuration precedence: built-in defaults → optional config YAML → environment variables (env/.env take final priority).
    • Automatic discovery of provider API keys from environment variables and improved provider configuration support.
    • Added a comprehensive example YAML template covering server, cache, storage, logging, metrics, HTTP, and providers.
  • Chores

    • Removed tracked config.yaml and added it to ignore; an example/template is provided instead.
    • .env template now comments previously active default lines (defaults no longer pre-set).
  • Documentation

    • Added full configuration guide with examples and variable expansion syntax.

…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>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 9, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Replaces 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

Cohort / File(s) Summary
Core config implementation
config/config.go
Replace Viper with defaultConfig + applyYAML + applyEnvVars + applyProviderEnvVars + removeEmptyProviders; add HTTPConfig; add YAML tags to config structs; add env-mapping helpers and known provider discovery.
Config examples & templates
config/config.example.yaml, config/config.yaml, .env.template, .gitignore
Add config.example.yaml, remove config/config.yaml, comment default entries in .env.template, and add /config/config.yaml to .gitignore.
Tests for config flow
config/..._test.go, config/config_helpers_test.go, config/config_test.go, config/config_example_test.go, config/config_defaults_test.go
Delete legacy defaults test; restructure and expand tests to validate defaults → YAML → env precedence, provider discovery/filtering, HTTP/cache settings, and various edge cases.
Docs & site navigation
docs/advanced/configuration.mdx, docs/docs.json
Add comprehensive configuration docs describing the three-layer pipeline, examples, and provider auto-discovery; update docs.json navigation.
Dependencies
go.mod
Remove Viper/mapstructure and related indirects; add direct gopkg.in/yaml.v3 and clean up indirect dependencies.
Providers & HTTP client
internal/providers/init.go, internal/httpclient/client.go
Switch local cache path to use cfg.Cache.CacheDir (default .cache); add a doc note about HTTP config/.env loading timing.

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)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

🐰 I hopped through defaults, YAML, then env,

nibbling keys and merging them in,
Providers found and extras swept away,
Configs tidy for a bright new day,
🥕 — lovingly signed, a tiny rabbit.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: refactoring the config system to use a unified 3-layer pipeline and adding configuration documentation. It directly corresponds to the primary objectives of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch gomodel-config

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🔵 Trivial

Dual env-var read path for HTTP timeouts — consider consolidating.

DefaultConfig() reads HTTP_TIMEOUT and HTTP_RESPONSE_HEADER_TIMEOUT directly from the environment, while config.HTTPConfig also maps these same env vars via applyEnvVars. The loaded config values are never passed to NewHTTPClient; providers instead call NewDefaultHTTPClient(), which triggers a fresh environment read. If config.HTTPConfig is 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.

Comment on lines +549 to +556
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))
}
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +559 to 589
// 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")
}
})
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Comment on lines +9 to +46
// 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)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Comment on lines +48 to +61
// 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)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Comment on lines +30 to +37
# 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +249 to +252
func Load() (*Config, error) {
// 1. Load .env into process env (ignore if not found)
_ = godotenv.Load()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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:


🏁 Script executed:

# Check the current code in config/config.go around the mentioned lines
find . -name "config.go" -type f | head -20

Repository: 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.go

Repository: 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.

Suggested change
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.

Comment on lines +416 to +450
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,
}
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +466 to 494
// 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"
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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).

Comment on lines +42 to +46
| 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)_ |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 -20

Repository: 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.

Comment on lines +164 to +188
```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: "..."
```
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant