fix(config): preserve explicit custom provider against legacy PROVIDER override

This commit is contained in:
Chummy 2026-02-19 16:58:01 +08:00
parent d6dca4b890
commit 9381e4451a
3 changed files with 67 additions and 4 deletions

View file

@ -20,6 +20,19 @@ Schema export command:
| `default_model` | `anthropic/claude-sonnet-4-6` | model routed through selected provider |
| `default_temperature` | `0.7` | model temperature |
## Environment Provider Overrides
Provider selection can also be controlled by environment variables. Precedence is:
1. `ZEROCLAW_PROVIDER` (explicit override, always wins when non-empty)
2. `PROVIDER` (legacy fallback, only applied when config provider is unset or still `openrouter`)
3. `default_provider` in `config.toml`
Operational note for container users:
- If your `config.toml` sets an explicit custom provider like `custom:https://.../v1`, a default `PROVIDER=openrouter` from Docker/container env will no longer replace it.
- Use `ZEROCLAW_PROVIDER` when you intentionally want runtime env to override a non-default configured provider.
## `[agent]`
| Key | Default | Purpose |

View file

@ -2767,13 +2767,23 @@ impl Config {
}
}
// Provider: ZEROCLAW_PROVIDER or PROVIDER
if let Ok(provider) =
std::env::var("ZEROCLAW_PROVIDER").or_else(|_| std::env::var("PROVIDER"))
{
// Provider override precedence:
// 1) ZEROCLAW_PROVIDER always wins when set.
// 2) Legacy PROVIDER is only honored when config still uses the
// default provider (openrouter) or provider is unset. This prevents
// container defaults from overriding explicit custom providers.
if let Ok(provider) = std::env::var("ZEROCLAW_PROVIDER") {
if !provider.is_empty() {
self.default_provider = Some(provider);
}
} else if let Ok(provider) = std::env::var("PROVIDER") {
let should_apply_legacy_provider = self.default_provider.as_deref().map_or(
true,
|configured| configured.trim().eq_ignore_ascii_case("openrouter"),
);
if should_apply_legacy_provider && !provider.is_empty() {
self.default_provider = Some(provider);
}
}
// Model: ZEROCLAW_MODEL or MODEL
@ -4353,6 +4363,42 @@ default_temperature = 0.7
std::env::remove_var("PROVIDER");
}
#[test]
fn env_override_provider_fallback_does_not_replace_non_default_provider() {
let _env_guard = env_override_test_guard();
let mut config = Config {
default_provider: Some("custom:https://proxy.example.com/v1".to_string()),
..Config::default()
};
std::env::remove_var("ZEROCLAW_PROVIDER");
std::env::set_var("PROVIDER", "openrouter");
config.apply_env_overrides();
assert_eq!(
config.default_provider.as_deref(),
Some("custom:https://proxy.example.com/v1")
);
std::env::remove_var("PROVIDER");
}
#[test]
fn env_override_zero_claw_provider_overrides_non_default_provider() {
let _env_guard = env_override_test_guard();
let mut config = Config {
default_provider: Some("custom:https://proxy.example.com/v1".to_string()),
..Config::default()
};
std::env::set_var("ZEROCLAW_PROVIDER", "openrouter");
std::env::set_var("PROVIDER", "anthropic");
config.apply_env_overrides();
assert_eq!(config.default_provider.as_deref(), Some("openrouter"));
std::env::remove_var("ZEROCLAW_PROVIDER");
std::env::remove_var("PROVIDER");
}
#[test]
fn env_override_glm_api_key_for_regional_aliases() {
let _env_guard = env_override_test_guard();

View file

@ -1194,6 +1194,8 @@ mod tests {
idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_secs(300), 1000)),
whatsapp: None,
whatsapp_app_secret: None,
linq: None,
linq_signing_secret: None,
observer: Arc::new(crate::observability::NoopObserver),
};
@ -1235,6 +1237,8 @@ mod tests {
idempotency_store: Arc::new(IdempotencyStore::new(Duration::from_secs(300), 1000)),
whatsapp: None,
whatsapp_app_secret: None,
linq: None,
linq_signing_secret: None,
observer,
};