feat: 3-tier provider credential resolution (env var → .env → stored config)#3211
Open
TheArchitectit wants to merge 2 commits into
Open
feat: 3-tier provider credential resolution (env var → .env → stored config)#3211TheArchitectit wants to merge 2 commits into
TheArchitectit wants to merge 2 commits into
Conversation
…config) Before this change, the setup wizard (PR ultraworkers#3017) saved provider config to ~/.claw/settings.json, but no provider ever read it back. The wizard was dead code: users who ran it saw no effect. This commit implements the 3-tier credential resolution chain that makes stored provider config functional: 1. Environment variable (highest priority, immediate override) - ANTHROPIC_API_KEY, OPENAI_API_KEY, XAI_API_KEY, DASHSCOPE_API_KEY - ANTHROPIC_BASE_URL, OPENAI_BASE_URL, etc. 2. .env file in the current working directory (via existing dotenv_value) 3. Stored provider config in ~/.claw/settings.json (lowest priority) - Reads provider.kind, provider.apiKey, provider.baseUrl - Only returned when stored kind matches the provider being resolved Changes: - RuntimeProviderConfig: new struct with kind/api_key/base_url fields parsed from the 'provider' JSON object in settings.json - RuntimeFeatureConfig: added provider field - RuntimeConfig::provider(): accessor for stored provider config - parse_optional_provider_config(): extracts provider section from merged settings - read_env_or_config() in mod.rs: core 3-tier resolution function - read_base_url_from_config() in mod.rs: base URL resolution from stored config - AuthSource::from_env_or_saved() in anthropic.rs: now actually reads from stored config (tier 3) when env vars are absent - resolve_startup_auth_source(): same 3-tier treatment - has_auth_from_env_or_saved(): also checks stored config - read_base_url() in anthropic.rs: 3-tier base URL resolution - OpenAiCompatClient::from_env_or_saved(): new method with 3-tier resolution for OpenAI/xAI/DashScope providers - read_base_url() in openai_compat.rs: 3-tier base URL resolution - 3 new tests for RuntimeProviderConfig parsing The setup wizard saves settings that providers now actually consume. Users without env vars can configure providers entirely through the wizard or by editing ~/.claw/settings.json manually.
This was referenced Jun 2, 2026
Open
|
This makes the setup wizard actually functional — good catch that #3017 saved config but nothing read it. The 3-tier priority (env var → .env → stored) is well thought out, and covering all four providers (Anthropic, OpenAI, xAI, DashScope) with both API key and base URL resolution is thorough.\n\nMinor concern: the rom_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 — a quick cargo fmt --all before merge will fix that. |
Addresses reviewer feedback on ultraworkers#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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
PR #3017 merged
setup_wizard.rswhich saves provider config (kind, apiKey, baseUrl, model) to~/.claw/settings.json. However, no provider ever reads that stored config back. The credential resolution path is purely env-var-based:AuthSource::from_env_or_saved()— Despite its name, never reads from stored configresolve_startup_auth_source()— Discards its OAuth config callback (let _ = load_oauth_config;) and reads only env varsOpenAiCompatClient::from_env()— Reads only from env varsread_base_url()(both providers) — Reads only from env varsResult: Users who run the setup wizard see no effect. The wizard is dead code.
Solution
Implement a 3-tier credential resolution chain:
.envfile in current working directory~/.claw/settings.jsonHow it works
The stored provider kind must match the provider being resolved for — if settings.json has
kind: "xai"and we are resolving credentials for the Anthropic provider, tier 3 returnsNone. This prevents cross-provider credential leakage.Changes
New types
RuntimeProviderConfig— struct withkind,api_key,base_urlfields parsed from the"provider"JSON object in settings.jsonRuntimeConfig::provider()— accessor for stored provider configparse_optional_provider_config()— extracts provider section from merged settingsUpdated functions
AuthSource::from_env_or_saved()— Now actually reads from stored config (tier 3) when env vars are absent. Previously the "or_saved" name was aspirational — the function was identical tofrom_env().resolve_startup_auth_source()— Same 3-tier treatment. No longer discards the OAuth config callback (kept for API compatibility).has_auth_from_env_or_saved()— Also checks stored configread_base_url()(anthropic.rs) — 3-tier base URL resolutionread_base_url()(openai_compat.rs) — 3-tier base URL resolutionNew functions
read_env_or_config()(mod.rs) — Core 3-tier credential resolutionread_base_url_from_config()(mod.rs) — Base URL resolution from stored configOpenAiCompatClient::from_env_or_saved()— 3-tier resolution for OpenAI/xAI/DashScope providers (analogous to the Anthropic path)Tests
3 new tests for
RuntimeProviderConfigparsing:provider_config_default_is_empty_when_unset— empty settings → all fields Noneprovider_config_parses_kind_api_key_and_base_url— full provider section parsed correctlyprovider_config_handles_partial_provider_object— only kind+apiKey, no baseUrlDiff verification
All additions, no deletions. No upstream commits are reverted. The diff is feature-only.
Settings.json structure
{ "provider": { "kind": "anthropic", "apiKey": "sk-ant-...", "baseUrl": "https://api.anthropic.com" }, "model": "sonnet" }This is exactly the format that
save_user_provider_settings()(already on main from PR #3017) writes.Testing