Skip to content

feat(config): accept GOMODEL_-prefixed env vars, deprecate bare names#541

Open
SantiagoDePolonia wants to merge 3 commits into
mainfrom
feat/env-prefix-compat
Open

feat(config): accept GOMODEL_-prefixed env vars, deprecate bare names#541
SantiagoDePolonia wants to merge 3 commits into
mainfrom
feat/env-prefix-compat

Conversation

@SantiagoDePolonia

@SantiagoDePolonia SantiagoDePolonia commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

What

GoModel-defined environment variables are now canonically spelled GOMODEL_<NAME>. The unprefixed spelling still resolves, so no existing deployment breaks — but reading one logs a warning naming its replacement.

WARN deprecated environment variable: rename it before the next major release
     variable=SQLITE_PATH use=GOMODEL_SQLITE_PATH

Why

The environment namespace is global and flat — shared with every other process in the container. BASE_PATH, HTTP_TIMEOUT, STORAGE_TYPE, and LOGGING_ENABLED are land grabs on generic names that say nothing about who owns them, and they collide the first time an operator shares an env_file across Compose services or an envFrom ConfigMap across containers.

This follows the common convention — prefix what you own, go bare only to join someone else's convention. WordPress (WORDPRESS_DB_HOST), Apache (APACHE_RUN_USER), Grafana (GF_SERVER_HTTP_PORT), Vault (VAULT_ADDR), Ollama (OLLAMA_HOST). LiteLLM makes the same two-tier split: LITELLM_MASTER_KEY for its own config, bare OPENAI_API_KEY for the ecosystem.

GOMODEL_MASTER_KEY and GOMODEL_CACHE_DIR already followed the convention. "Everything bare except two" was the worst state — readers couldn't infer the rule either way.

What stays bare (permanently)

Reason
PORT, REDIS_URL PaaS platforms inject them. Railway/Heroku set PORT; their Redis plugins set REDIS_URL, so bare = zero-config wiring.
<PROVIDER>_API_KEY, <PROVIDER>_BASE_URL, <PROVIDER>_MODELS, ... Each vendor's namespace. OPENAI_API_KEY is the OpenAI SDK's own name — reading it bare is what makes GoModel drop-in. Generated from provider name + suffix, so it moves as one unit or not at all.
DOCKER_HOST, MOCK_*, RECORD, UPDATE_GOLDEN, TEST_* Test harness only.

POSTGRES_URL / MONGODB_URL are not exempt despite looking like ecosystem names — nothing injects them. The real convention is DATABASE_URL, which GoModel doesn't read today; accepting it would be a separate feature, not a rename.

Design notes for review

  • Why internal/envcompat and not config/: HTTP_TIMEOUT is read twice — by the config struct tags and independently by internal/httpclient, which can't import config.
  • Empty canonical doesn't shadow legacy. A non-empty value wins, canonical first. An unexpanded GOMODEL_SQLITE_PATH= in a compose file alongside a real SQLITE_PATH resolves to the real one instead of silently discarding it. Presence is still reported when only empty values are set, so ok-bool callers (RESPONSE_CACHE_SIMPLE_ENABLED= means "off") keep working.
  • Two resolution paths. Exact lookup covers the whole tagged config through the single os.Getenv in applyEnvOverridesValue. A prefix scan covers the four families discovered by walking os.Environ(): SET_BUDGET_*, SET_RATE_LIMIT_*, SET_PROVIDER_RATE_LIMIT_*, TAGGING_HEADER_<N>.
  • Scan sorts by suffix. Callers resolve suffixes to canonical keys and two suffixes can collide there (SET_RATE_LIMIT_A_ and SET_RATE_LIMIT_A both name path a), so iteration order decided the winner — previously dependent on OS ordering.
  • Scan reports the legacy spelling, so companions resolve back through envcompat. This is what lets a canonical GOMODEL_TAGGING_HEADER_1 pair with a legacy TAGGING_HEADER_1_PREFIX.
  • GOMODEL_CONFIG_STRICT governs the YAML parse and runs before the tag walker, so it calls envcompat at its own site.
  • LOG_LEVEL / LOG_FORMAT are included — they live in run/ rather than config/ and were missed by the original survey.

Verification

Beyond unit tests, run against the real binary with mixed spellings:

  • /health200 with bare PORT=18099 (exempt path intact)
  • GOMODEL_SQLITE_PATH created the DB at the canonical path (prefix drives real behavior)
  • GOMODEL_LOG_FORMAT=json took effect
  • Only the legacy LOGGING_LOG_BODIES warned; canonical vars stayed quiet

make test-race, make lint, and the hot-path alloc guard all pass.

Not in this PR

Documentation still shows the legacy spellings. Renaming that surface (.env.template, config.example.yaml, README, helm/, compose files) is a large mechanical diff that the exempt rules above make unsafe to do blindly — it's a follow-up. .env.template, CLAUDE.md, and docs/dev/2026-07-17_env-prefix-migration.md document the convention and the full mapping in the meantime.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added support for canonical GOMODEL_ environment-variable names.
    • Legacy unprefixed names still work temporarily and emit one-time migration warnings; canonical values take precedence when both are set.
    • Preserved permanent exceptions for PORT, REDIS_URL, and provider-scoped variables (with special-case handling where applicable).
    • Environment-based configuration (including caching, tagging headers, MCP servers, virtual models, keyed/rate-limit settings) now consistently honors the canonical naming.
    • Health and readiness probes no longer output logs.
  • Documentation
    • Added/expanded environment-variable migration guidance, mappings, precedence rules, and exceptions.
  • Tests
    • Added coverage for canonical/legacy env prefix resolution and override behavior.

GoModel-defined environment variables are now canonically spelled
GOMODEL_<NAME>. The unprefixed spelling still resolves, so no existing
deployment breaks, but reading one logs a warning naming its replacement.

The env namespace is global and flat. Names like BASE_PATH, HTTP_TIMEOUT,
STORAGE_TYPE, and LOGGING_ENABLED are land grabs on generic names that
collide the first time an operator shares an env_file across Compose
services or an envFrom ConfigMap across containers. GOMODEL_MASTER_KEY and
GOMODEL_CACHE_DIR already followed the convention; "everything bare except
two" left readers unable to infer the rule either way.

PORT and REDIS_URL keep their bare names because PaaS platforms inject
them, and the provider family (OPENAI_API_KEY, <PROVIDER>_BASE_URL, ...)
keeps its because those live in each vendor's namespace and are what make
GoModel drop-in compatible with their SDKs.

Resolution lives in internal/envcompat rather than config/ because
HTTP_TIMEOUT is read both by the config struct tags and independently by
internal/httpclient. A non-empty value wins, canonical first; an empty
canonical does not shadow a working legacy value, so an unexpanded
GOMODEL_SQLITE_PATH= in a compose file cannot silently discard a real
SQLITE_PATH.

Two resolution paths exist: exact lookup, reached for the whole tagged
config through the single os.Getenv in applyEnvOverridesValue, and a
prefix scan for the four families discovered by walking os.Environ
(SET_BUDGET_*, SET_RATE_LIMIT_*, SET_PROVIDER_RATE_LIMIT_*,
TAGGING_HEADER_<N>). Scan sorts by suffix so two suffixes that resolve to
the same canonical key no longer depend on OS ordering, and reports the
legacy spelling so companions resolve through envcompat — letting a
canonical GOMODEL_TAGGING_HEADER_1 pair with a legacy
TAGGING_HEADER_1_PREFIX.

LOG_LEVEL and LOG_FORMAT are included; they are read in run/ rather than
config/ and were missing from the original survey.

Documentation still shows the legacy spellings. Renaming that surface is a
large mechanical diff the exempt rules make unsafe to do blindly, so it is
left to a follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mintlify

mintlify Bot commented Jul 17, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
gomodel 🟢 Ready View Preview Jul 17, 2026, 11:30 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@mintlify

mintlify Bot commented Jul 17, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
gomodel 🟡 Building Jul 17, 2026, 11:29 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@codecov-commenter

codecov-commenter commented Jul 17, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 75.00000% with 29 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
config/cache.go 24.00% 0 Missing and 19 partials ⚠️
internal/envcompat/envcompat.go 88.23% 6 Missing and 2 partials ⚠️
config/user_path_env.go 66.66% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Jul 17, 2026

Copy link
Copy Markdown

Confidence Score: 4/5

Mostly safe, with one contained config-resolution bug to fix before merging.

Exact env lookup behavior is consistent with the migration rules. Dynamic env scanning can drop a non-empty legacy value when the canonical value is present but empty.

internal/envcompat/envcompat.go

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex ran the requested verification, but its local artifact references were not uploaded.
  • Build proof confirms gomodel was built successfully with a go build exit status of 0.
  • Runtime proof confirms the server started with the provider, bound to :19082, and the expected LOGGING_LOG_BODIES deprecation warning appeared.
  • Health proof confirms the /health endpoint returns 200 OK with the status ok.
  • DB proof confirms the SQLite database file exists and is 303104 bytes in size.

View all artifacts

T-Rex Ran code and verified through T-Rex

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant App as GoModel startup/runtime
participant Exact as Exact env readers
participant Scan as Dynamic env scanners
participant Env as Process environment
participant Cfg as Resolved config

App->>Exact: envcompat.Get/Lookup(NAME)
Exact->>Env: read GOMODEL_NAME
alt canonical non-empty
    Exact-->>Cfg: canonical value
else legacy non-empty
    Exact->>Env: read NAME
    Exact-->>Cfg: legacy value + warning
else only empty value present
    Exact-->>Cfg: "empty with ok=true"
end

App->>Scan: envcompat.Scan(PREFIX_)
Scan->>Env: "walk GOMODEL_PREFIX_* and PREFIX_*"
Scan-->>Cfg: sorted entries by suffix
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant App as GoModel startup/runtime
participant Exact as Exact env readers
participant Scan as Dynamic env scanners
participant Env as Process environment
participant Cfg as Resolved config

App->>Exact: envcompat.Get/Lookup(NAME)
Exact->>Env: read GOMODEL_NAME
alt canonical non-empty
    Exact-->>Cfg: canonical value
else legacy non-empty
    Exact->>Env: read NAME
    Exact-->>Cfg: legacy value + warning
else only empty value present
    Exact-->>Cfg: "empty with ok=true"
end

App->>Scan: envcompat.Scan(PREFIX_)
Scan->>Env: "walk GOMODEL_PREFIX_* and PREFIX_*"
Scan-->>Cfg: sorted entries by suffix
Loading

Reviews (1): Last reviewed commit: "feat(config): accept GOMODEL_-prefixed e..." | Re-trigger Greptile

Comment thread internal/envcompat/envcompat.go Outdated
Comment on lines +116 to +140
bySuffix := make(map[string]Entry)
legacy := make(map[string]string)

for _, kv := range os.Environ() {
key, value, ok := strings.Cut(kv, "=")
if !ok {
continue
}
switch {
case strings.HasPrefix(key, canonicalPrefix):
suffix := key[len(canonicalPrefix):]
bySuffix[suffix] = Entry{Name: prefix + suffix, Suffix: suffix, Value: value}
case strings.HasPrefix(key, prefix):
legacy[key[len(prefix):]] = value
}
}

for suffix, value := range legacy {
if _, canonicalSet := bySuffix[suffix]; canonicalSet {
continue
}
name := prefix + suffix
warn(name, Prefix+name)
bySuffix[suffix] = Entry{Name: name, Suffix: suffix, Value: value}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Preserve empty fallback
Scan treats any canonical spelling as authoritative, even when its value is empty. With GOMODEL_SET_RATE_LIMIT_TEAM= and SET_RATE_LIMIT_TEAM=rpm=10, line 134 drops the non-empty legacy value, then applyKeyedLimitEnv skips the empty entry, so the configured limit disappears. This contradicts Lookup and the migration rule that empty canonical values do not shadow working legacy values.

Context Used: CLAUDE.md (source)

T-Rex Ran code and verified through T-Rex

…ng paths

- Scan now resolves every discovered suffix through the same lookup as
  Lookup, so a blank GOMODEL_SET_BUDGET_* / SET_RATE_LIMIT_* canonical
  no longer silently discards a working legacy rule, and Entry.Name is
  the spelling that actually supplied the value, so startup errors name
  a variable that exists in the operator's environment.
- Whitespace-only canonical values no longer shadow working legacy
  values (every caller trims before use).
- OPENROUTER_SITE_URL / OPENROUTER_APP_NAME route through envcompat:
  the GOMODEL_ spellings resolve and the bare ones warn (they are
  GoModel-defined attribution config, not OpenRouter's namespace).
- LOG_LEVEL / LOG_FORMAT deprecation warnings are deferred until the
  configured slog handler is installed (new envcompat.Quiet), so a JSON
  deployment gets JSON warnings instead of one unparseable text line.
- --health/--ready probes discard slog output, so periodic healthchecks
  no longer re-emit the full deprecation warning set every probe.
- Setting the prefixed spelling of an exempt name (GOMODEL_PORT) warns
  once that it is not read instead of silently binding the default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 31921331-7fea-4d9c-a5f6-eb1e9843464d

📥 Commits

Reviewing files that changed from the base of the PR and between 9b9626d and 1782b14.

📒 Files selected for processing (10)
  • config/config_test.go
  • config/env.go
  • config/env_prefix_test.go
  • config/tagging.go
  • internal/envcompat/envcompat.go
  • internal/envcompat/envcompat_test.go
  • internal/mcpgateway/upstream.go
  • internal/providers/anthropic/anthropic_test.go
  • internal/providers/anthropic/request_translation.go
  • run/providers_test.go
💤 Files with no reviewable changes (1)
  • config/env_prefix_test.go

📝 Walkthrough

Walkthrough

The PR adds a GOMODEL_ environment-variable compatibility layer with canonical precedence, legacy fallback warnings, exemptions, and dynamic scanning. Configuration, provider, HTTP, and logging consumers now use it, with documentation and comprehensive tests.

Changes

Environment Prefix Migration

Layer / File(s) Summary
Compatibility resolver and migration contract
.env.template, CLAUDE.md, docs/dev/..., internal/envcompat/*
Defines canonical and legacy resolution, exemptions, warning behavior, dynamic scanning, migration rules, and comprehensive resolver tests.
Configuration lookup and dynamic-family integration
config/*
Routes configuration, cache, MCP, virtual-model, tagging, and keyed-limit environment handling through envcompat, with canonical/legacy integration tests.
Runtime and provider environment integration
internal/httpclient/*, internal/providers/*, run/*
Updates runtime and provider environment reads, verifies canonical OpenRouter attribution variables, memoizes Anthropic defaults, and suppresses health/readiness probe logging.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Process
  participant ConfigLoader
  participant envcompat
  participant slog
  Process->>ConfigLoader: Load configuration
  ConfigLoader->>envcompat: Resolve GOMODEL or legacy variables
  envcompat->>slog: Emit deduplicated legacy warning
  envcompat-->>ConfigLoader: Return resolved values
  ConfigLoader-->>Process: Apply configuration
Loading

Possibly related PRs

Poem

A rabbit found a prefix bright,
And hopped through vars from morn till night.
Bare names warn, new names glow,
Canonical values lead the show.
Caches, logs, and models cheer—
GOMODEL_ makes paths clear!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main change: canonical GOMODEL_ env vars with bare names retained but deprecated.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/env-prefix-compat

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.

@SantiagoDePolonia

Copy link
Copy Markdown
Contributor Author

Follow-up commit 9b9626d1 closes the findings from the multi-agent review:

  • Scan now resolves through the same lookup as Lookup — a blank canonical (GOMODEL_SET_BUDGET_TEAM= from an unexpanded compose variable) no longer discards a working legacy rule, and the two code paths can no longer diverge. Entry.Name is now the spelling that actually supplied the value, so startup errors name a variable that exists in the operator's environment.
  • Whitespace-only canonical values no longer shadow working legacy values.
  • OPENROUTER_SITE_URL / OPENROUTER_APP_NAME (the one missed call site repo-wide) now route through envcompat; canonical spellings resolve, bare ones warn. Documented as judgment calls in the migration doc.
  • LOG_LEVEL/LOG_FORMAT warnings are deferred (new envcompat.Quiet) until the configured slog handler is installed — a JSON deployment now gets JSON warning lines instead of one unparseable bootstrap-text line. Verified end-to-end.
  • --health/--ready probes discard slog output, so periodic healthchecks no longer re-emit the full deprecation warning set every 30s.
  • GOMODEL_PORT/GOMODEL_REDIS_URL now warn once that they are not read, instead of silently binding defaults.

New regression tests cover each behavior (blank-canonical Scan fallthrough, whitespace shadowing, Quiet-then-Get warn-once, prefixed-exempt warning, canonical GOMODEL_OPENROUTER_* spellings).

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.env.template:
- Around line 4-5: Update the canonical-versus-legacy precedence documentation
to state that the canonical GOMODEL_ value wins when nonblank, but a blank or
whitespace-only canonical value falls back to a nonblank legacy value; apply
this wording consistently at .env.template lines 4-5 and CLAUDE.md lines
109-110.

In `@internal/envcompat/envcompat.go`:
- Around line 77-86: Update Lookup to detect when a prefixed input strips to an
exempt bare name before the generic prefixed-name branch, and reject it without
reading the environment value. Preserve the existing warning behavior for bare
exempt lookups, and extend TestPrefixedExemptNameWarns to verify the
already-prefixed call form.

In `@run/health.go`:
- Around line 19-29: Make slog suppression scoped to each probe instead of
permanently changing the process-wide logger. Update runHealthProbe and
runReadyProbe to save slog.Default(), install the discard logger, and defer
restoration of the saved logger for the probe’s full execution.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5a9c44fb-978f-4c67-a220-8607c1954ed0

📥 Commits

Reviewing files that changed from the base of the PR and between 23cdb25 and 9b9626d.

📒 Files selected for processing (22)
  • .env.template
  • CLAUDE.md
  • config/cache.go
  • config/config.go
  • config/config_test.go
  • config/env.go
  • config/env_prefix_test.go
  • config/mcp.go
  • config/tagging.go
  • config/user_path_env.go
  • config/virtualmodels.go
  • docs/dev/2026-07-17_env-prefix-migration.md
  • internal/envcompat/envcompat.go
  • internal/envcompat/envcompat_test.go
  • internal/httpclient/client.go
  • internal/providers/anthropic/request_translation.go
  • internal/providers/gemini/gemini.go
  • internal/providers/opencodego/opencodego.go
  • internal/providers/openrouter/openrouter.go
  • internal/providers/openrouter/openrouter_test.go
  • run/health.go
  • run/logging.go

Comment thread .env.template
Comment on lines +4 to +5
# they will be removed in a future major release. Setting both spellings
# resolves to the GOMODEL_ one.

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document the actual canonical-versus-legacy precedence.

Both summaries say canonical always wins when both spellings exist, but a blank or whitespace-only canonical value falls back to a nonblank legacy value.

  • .env.template#L4-L5: add the blank-canonical fallback exception.
  • CLAUDE.md#L109-L110: describe the same exception consistently.
📍 Affects 2 files
  • .env.template#L4-L5 (this comment)
  • CLAUDE.md#L109-L110
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.env.template around lines 4 - 5, Update the canonical-versus-legacy
precedence documentation to state that the canonical GOMODEL_ value wins when
nonblank, but a blank or whitespace-only canonical value falls back to a
nonblank legacy value; apply this wording consistently at .env.template lines
4-5 and CLAUDE.md lines 109-110.

Source: Coding guidelines

Comment on lines +77 to +86
if exempt[name] {
if _, prefixedSet := os.LookupEnv(Prefix + name); prefixedSet && !quiet {
warnPrefixedExempt(name)
}
value, ok = os.LookupEnv(name)
return value, ok, name
}
if strings.HasPrefix(name, Prefix) {
value, ok = os.LookupEnv(name)
return value, ok, name

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject already-prefixed exempt names too.

Lookup("GOMODEL_PORT") reaches the generic prefixed-name branch and reads the value, contradicting the contract that GOMODEL_PORT is never honored. Check the stripped name against exempt before accepting prefixed inputs, and extend TestPrefixedExemptNameWarns to cover this call form.

Proposed fix
 	if exempt[name] {
 		if _, prefixedSet := os.LookupEnv(Prefix + name); prefixedSet && !quiet {
 			warnPrefixedExempt(name)
 		}
 		value, ok = os.LookupEnv(name)
 		return value, ok, name
 	}
 	if strings.HasPrefix(name, Prefix) {
+		if bare := strings.TrimPrefix(name, Prefix); exempt[bare] {
+			if _, set := os.LookupEnv(name); set && !quiet {
+				warnPrefixedExempt(bare)
+			}
+			return "", false, name
+		}
 		value, ok = os.LookupEnv(name)
 		return value, ok, name
 	}

As per coding guidelines, PORT and REDIS_URL are permanently bare variables.

📝 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
if exempt[name] {
if _, prefixedSet := os.LookupEnv(Prefix + name); prefixedSet && !quiet {
warnPrefixedExempt(name)
}
value, ok = os.LookupEnv(name)
return value, ok, name
}
if strings.HasPrefix(name, Prefix) {
value, ok = os.LookupEnv(name)
return value, ok, name
if exempt[name] {
if _, prefixedSet := os.LookupEnv(Prefix + name); prefixedSet && !quiet {
warnPrefixedExempt(name)
}
value, ok = os.LookupEnv(name)
return value, ok, name
}
if strings.HasPrefix(name, Prefix) {
if bare := strings.TrimPrefix(name, Prefix); exempt[bare] {
if _, set := os.LookupEnv(name); set && !quiet {
warnPrefixedExempt(bare)
}
return "", false, name
}
value, ok = os.LookupEnv(name)
return value, ok, name
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/envcompat/envcompat.go` around lines 77 - 86, Update Lookup to
detect when a prefixed input strips to an exempt bare name before the generic
prefixed-name branch, and reject it without reading the environment value.
Preserve the existing warning behavior for bare exempt lookups, and extend
TestPrefixedExemptNameWarns to verify the already-prefixed call form.

Source: Coding guidelines

Comment thread run/health.go
Comment on lines +19 to +29
// silenceProbeLogging discards slog output for the short-lived --health and
// --ready processes. They load config without configureLogging, so anything
// slog writes — such as the envcompat deprecation warnings, re-emitted by
// every fresh probe process — would otherwise pollute periodic healthcheck
// output through the bootstrap text handler.
func silenceProbeLogging() {
slog.SetDefault(slog.New(slog.DiscardHandler))
}

func runHealthProbe(timeout time.Duration) error {
silenceProbeLogging()

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Restore the process-wide logger after each probe.

silenceProbeLogging permanently discards all subsequent slog output in the process, affecting embedded callers and later tests. Save the previous logger and restore it with defer.

Proposed fix
-func silenceProbeLogging() {
+func silenceProbeLogging() func() {
+	previous := slog.Default()
 	slog.SetDefault(slog.New(slog.DiscardHandler))
+	return func() { slog.SetDefault(previous) }
 }

 func runHealthProbe(timeout time.Duration) error {
-	silenceProbeLogging()
+	restoreLogging := silenceProbeLogging()
+	defer restoreLogging()

Apply the same scoped restoration in runReadyProbe.

As per coding guidelines, “Configuration and runtime behavior must preserve stateless request handling and avoid unsafe reliance on hidden global state.”

Also applies to: 45-47

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@run/health.go` around lines 19 - 29, Make slog suppression scoped to each
probe instead of permanently changing the process-wide logger. Update
runHealthProbe and runReadyProbe to save slog.Default(), install the discard
logger, and defer restoration of the saved logger for the probe’s full
execution.

Source: Coding guidelines

Cleanup pass over the envcompat change, no behavior changes:

- collapse warn/warnPrefixedExempt into one warnOnce helper
- drop the redundant tagging-header index regex (Atoi already rejects
  every suffix it filtered)
- memoize the Anthropic default-max-tokens env read with sync.OnceValue,
  matching how gemini/opencodego resolve at construction; tests that
  flip the var reset the memo
- replace the bespoke unset() test helper with the t.Setenv idiom used
  elsewhere, drop test env clears that the shared helper already covers,
  and move VIRTUAL_MODELS/MCP_SERVERS into clearAllConfigEnvVars
- add a guard test asserting no config env tag names a provider-family
  variable, pinning the exemption the migration doc promises
- comment the deliberate envcompat bypasses in ${VAR} expansion and the
  MCP stdio subprocess environment
- merge the envcompat import into the existing import group in three
  provider files

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants