# 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_saved` naming 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 config
- `from_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 only**
- `from_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