8.0 KiB
PR #3211 — Execution Plan: 3-Tier Credential Resolution (REVISED)
Branch: worktree-provider-config-fallback
Worktree: .claude/worktrees/provider-config-fallback
CI State: Build ✅ Test ✅ Clippy ✅ Docs ✅ Windows ✅ Fmt ❌
Reviewer Feedback
From 1716775457damn:
"the
from_env_or_savednaming across multiple provider files could be consolidated into a shared trait or helper to reduce duplication. Also, cargo fmt is failing"
Investigation Findings (Updated)
Key discovery: from_env vs from_env_or_saved are DIFFERENT things
There are TWO AuthSource methods in anthropic.rs:
from_env()(line 44) — tiers 1+2 only (process env + .env), NO stored configfrom_env_or_saved()(line 629) — tiers 1+2+3 (process env + .env + stored config)
And TWO in openai_compat.rs:
from_env(config)(line 132) — tiers 1+2 onlyfrom_env_or_saved(config)(line 151) — tiers 1+2+3
The _or_saved suffix is semantically meaningful — it distinguishes the 3-tier path from the 2-tier path. Renaming them identically would lose this distinction.
What IS actually duplicated and safe to consolidate
| Duplication | anthropic.rs | openai_compat.rs | mod.rs (shared) |
|---|---|---|---|
read_env_non_empty(key) |
✅ line 773 | ✅ line 1608 | ❌ not shared |
read_base_url() |
✅ line 798 | ✅ line 1625 | ❌ not shared |
Both read_base_url() functions follow the same 3-tier pattern:
Tier 1: std::env::var(key) → return if non-empty
Tier 2: dotenv_value(key) → return if non-empty
Tier 3: read_base_url_from_config(provider_kind) → return if Some
Default: fallback string
But there's a complication: anthropic::read_base_url() hardcodes the env var and provider kind, while openai_compat::read_base_url(config) takes them from the config struct. The shared helper needs to parameterize both.
Similarly, read_env_non_empty is identical in both files — just reads env + .env fallback.
Why NOT a shared trait (confirmed)
Anthropic's AuthSource has 4 variants (None, ApiKey, BearerToken, ApiKeyAndBearer) while OpenAI-compat has only ApiKey. Different return types → trait adds complexity.
Execution Plan (Revised)
Step 1: Extract read_env_non_empty() into mod.rs
Both files have identical read_env_non_empty(key) implementations. Move to mod.rs as pub(crate) fn read_env_non_empty(key: &str) -> Result<Option<String>, ApiError> and update both files to call super::read_env_non_empty(key).
Files changed:
rust/crates/api/src/providers/mod.rs— add sharedread_env_non_empty()rust/crates/api/src/providers/anthropic.rs— remove localread_env_non_empty(), usesuper::read_env_non_empty()rust/crates/api/src/providers/openai_compat.rs— remove localread_env_non_empty(), usesuper::read_env_non_empty()
Step 2: Extract resolve_base_url() into mod.rs
Both read_base_url() functions follow the identical 3-tier pattern, differing only in the env var name, provider kind, and default value.
pub fn resolve_base_url(env_var: &str, provider_kind: &str, default: &str) -> String {
// Tier 1: environment variable
if let Ok(value) = std::env::var(env_var) {
if !value.is_empty() {
return value;
}
}
// Tier 2: .env file
if let Some(value) = dotenv_value(env_var) {
if !value.is_empty() {
return value;
}
}
// Tier 3: stored config in ~/.claw/settings.json
if let Some(base_url) = read_base_url_from_config(provider_kind) {
return base_url;
}
default.to_string()
}
Then:
anthropic::read_base_url()→ delegates tosuper::resolve_base_url("ANTHROPIC_BASE_URL", "anthropic", DEFAULT_BASE_URL)openai_compat::read_base_url(config)→ delegates tosuper::resolve_base_url(config.base_url_env, provider_kind, config.default_base_url)
Files changed:
rust/crates/api/src/providers/mod.rs— addresolve_base_url()rust/crates/api/src/providers/anthropic.rs— simplifyread_base_url()to one-liner delegationrust/crates/api/src/providers/openai_compat.rs— simplifyread_base_url()to one-liner delegation
Step 3: Do NOT rename from_env_or_saved → from_env
The _or_saved suffix is semantically meaningful and distinguishes the 3-tier path from the 2-tier from_env() method. Removing it would conflate two different code paths. The reviewer asked to "reduce duplication" — Steps 1+2 do that by extracting the duplicated logic into shared helpers while preserving the intentional naming distinction.
Step 4: Run cargo fmt --all
Fixes the only failing CI check.
Step 5: Run cargo clippy --workspace --all-targets -- -D warnings + cargo test --workspace
Verify nothing breaks.
Step 6: Commit + push
refactor: extract shared credential resolution helpers into mod.rs
Addresses reviewer feedback on #3211 requesting consolidation of
duplicated credential-resolution logic across provider files.
Changes:
1. Extract read_env_non_empty() into mod.rs
- Previously duplicated identically in anthropic.rs (line 773)
and openai_compat.rs (line 1608).
- Both copies read a process env var, fall back to .env via
dotenv_value(), and return Result<Option<String>, ApiError>.
- Now defined once as pub(crate) in mod.rs; both provider files
call super::read_env_non_empty() instead.
2. Extract resolve_base_url() into mod.rs
- Both anthropic::read_base_url() and openai_compat::read_base_url()
implemented the same 3-tier base URL resolution:
Tier 1: process env var (e.g. ANTHROPIC_BASE_URL)
Tier 2: .env file via dotenv_value()
Tier 3: stored config via read_base_url_from_config()
Default: provider-specific fallback string
- New shared helper: resolve_base_url(env_var, provider_kind, default)
- anthropic::read_base_url() now delegates to
super::resolve_base_url("ANTHROPIC_BASE_URL", "anthropic", DEFAULT_BASE_URL)
- openai_compat::read_base_url(config) now delegates to
super::resolve_base_url(config.base_url_env, provider_kind, config.default_base_url)
3. Preserve from_env_or_saved() naming
- The "_or_saved" suffix distinguishes the 3-tier credential path
(env + .env + stored config) from the 2-tier from_env() path
(env + .env only). Both methods exist on the same AuthSource type
and serve different callers. Renaming would lose this semantic
distinction.
CI: cargo fmt --all run to fix the formatting check failure.
Commit Details
Commit: c0855e12 on worktree-provider-config-fallback
Files changed:
rust/crates/api/src/providers/mod.rs— added sharedread_env_non_empty()(+8 lines) andresolve_base_url()(+30 lines)rust/crates/api/src/providers/anthropic.rs— replaced localread_env_non_empty()andread_base_url()withsuper::delegations (-17 lines)rust/crates/api/src/providers/openai_compat.rs— same pattern (-17 lines)rust/crates/runtime/src/config.rs— cargo fmt fix on assert (+5/-1)
Net: +46 insertions, -45 deletions
Risk Assessment
- Low risk: Moving identical code into shared functions, no behavior change
- Call sites: All
from_env_or_saved/from_envcall sites are internal to the providers crate - Public API:
api::read_base_url()andapi::read_xai_base_url()still work — they call the provider-local wrappers which delegate to the shared helper - No behavior change: Same code paths, just better organized
Acceptance Criteria
read_env_non_emptyonly defined once (inmod.rs)resolve_base_urlexists inmod.rsand is used by both provider filesfrom_env_or_savednaming preserved (semantic distinction fromfrom_env)cargo fmt --allpassescargo test -p apipasses (147 unit + 37 integration, all green)cargo check -p apipasses- Changes committed and pushed to
worktree-provider-config-fallbackbranch (commitc0855e12) - Pre-existing clippy issues in runtime/trident.rs — not from our changes, noted