Skip to content

fix(providers): surface CLI logins as their own providers, not API-provider fallbacks#3059

Merged
houko merged 3 commits into
mainfrom
fix/cli-provider-detection
Apr 24, 2026
Merged

fix(providers): surface CLI logins as their own providers, not API-provider fallbacks#3059
houko merged 3 commits into
mainfrom
fix/cli-provider-detection

Conversation

@houko

@houko houko commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Two-commit fix for the provider-detection mess where CLI installs and CLI logins were being treated as API-provider credentials, causing false-positive detection and duplicate rows in the provider list.

Commit 1 · fix(llm-drivers): stop treating CLI config files as auth credentials

Bug: settings.json / wrong credential filenames were matched as proof of authentication.

Reported trigger: fresh install, GEMINI_API_KEY / GOOGLE_API_KEY unset, vault empty — Gemini still showed as auto-detected because ~/.gemini/settings.json (a preferences file created on first launch) was being checked.

  • gemini-cli: was checking settings.json (preference file, always exists) and .credentials.json (wrong filename). Now checks oauth_creds.json (Google Gemini CLI's actual OAuth token) + credentials.json / .credentials.json fallbacks.
  • claude-code: was checking settings.json with a comment claiming it covered keychain-auth users. settings.json is written before login, and the primary detect() path already probes the claude binary at PATH + /opt/homebrew/bin + /usr/local/bin + /usr/bin, so keychain users stay covered without the false-positive signal.
  • qwen-code: Qwen Code forks Gemini CLI — added oauth_creds.json alongside existing fallbacks to match logged-in users that were previously missed.
  • codex-cli: unchanged (already only checks ~/.codex/auth.json + validates token expiry).

Each detector now delegates to a *_credentials_in_dir(&Path) helper so tests drive it against a tempdir without HOME manipulation.

Commit 2 · fix(providers): stop promoting CLI logins into API provider auth

Bug: a CLI login silently promoted its corresponding API provider to Configured/AutoDetected:

  • claude-code login → anthropic marked ConfiguredCli
  • codex-cli login → openai marked AutoDetected (via read_codex_credential() extracting the token from ~/.codex/auth.json)
  • gemini-cli login → gemini marked ConfiguredCli
  • qwen-code login → qwen marked ConfiguredCli

Dashboard then showed each credential twice (once under the CLI provider, once under the API provider), and provider = "openai" could run silently on a Codex-extracted token without OPENAI_API_KEY ever being set.

New contract: CLI login surfaces only as its own CLI provider entry; API provider lights up only when the user sets its own API key.

  • model_catalog.rs::detect_auth: dropped has_cli_fallback and the read_codex_credential() clause from has_key_fallback. Renamed to has_key_alias — only Gemini's GOOGLE_API_KEY alias remains (Google AI Studio officially documents it as equivalent to GEMINI_API_KEY). Removed the now-unused pub fn read_codex_credential() helper.
  • drivers/mod.rs: runtime driver factory and resolve_provider_api_key() no longer fall back to Codex auth.json when OPENAI_API_KEY is empty — runtime matches detection.
  • providers.rs::test_provider: removed the dead ConfiguredCli branch; no API provider produces that status anymore.

AuthStatus::ConfiguredCli variant is retained (public enum) but unproduced; existing matches treat it as available so any stale serialised value round-trips harmlessly.

Behavior change — release-note worthy

Agents previously configured with provider = "openai" and no OPENAI_API_KEY were silently running on a Codex CLI token. Those will now return MissingApiKey. Migration: either set OPENAI_API_KEY, or switch the agent to provider = "codex-cli" (uses the subprocess path).

Test plan

  • cargo test -p librefang-llm-drivers --lib — 342 passed (10 new credential-detection tests)
  • cargo test -p librefang-runtime --lib — 2 new regression tests in model_catalog::tests pass
  • cargo clippy -p librefang-llm-drivers --all-targets -- -D warnings — clean
  • Manual: with only ~/.gemini/settings.json, confirm Gemini no longer appears as Configured in the provider list
  • Manual: with only ~/.codex/auth.json, confirm openai appears as Missing (but codex-cli still appears as Configured)
  • Manual: with OPENAI_API_KEY set, confirm openai appears as Configured

Follow-ups (not in this PR)

  • dashboard/src/pages/ProvidersPage.tsx / ChatPage.tsx still branch on "configured_cli" — dead code now, safe to clean up.
  • Naming: claude_credentials_exist / qwen_credentials_exist / gemini_cli_credentials_exist — three spellings for the same concept (pre-existing).

The CLI-provider fallback detectors for Gemini, Claude Code, and Qwen
Code were matching non-credential files, auto-marking providers as
"configured" for anyone who had merely installed the CLI.

- gemini-cli: was checking `settings.json` (a preferences file created
  on first launch) and `.credentials.json` (wrong filename). Now checks
  `oauth_creds.json` (the actual Google Gemini CLI OAuth token) plus
  defensive fallbacks.
- claude-code: was checking `settings.json` with a comment claiming it
  covered keychain-auth users. In practice settings.json is written
  without login, and the primary `detect()` path already probes the
  binary at PATH + common install locations, so keychain users are
  still covered. Dropped the false-positive signal.
- qwen-code: Qwen Code forks Gemini CLI, so added `oauth_creds.json`
  alongside the existing fallbacks to match logged-in users.

Each detector now delegates to a `*_credentials_in_dir(&Path)` helper
so the logic is directly unit-testable without HOME manipulation.
@github-actions github-actions Bot added size/L 250-999 lines changed ready-for-review PR is ready for maintainer review labels Apr 24, 2026
Previously a CLI login (Claude Code, Codex CLI, Gemini CLI, Qwen Code)
would silently mark its corresponding API provider (anthropic, openai,
gemini, qwen) as Configured/AutoDetected, causing the dashboard to
show each credential twice — once under the CLI provider and once
under the API provider — and letting `provider = "openai"` silently
run on a Codex-extracted token without the user ever setting
`OPENAI_API_KEY`.

New rule: CLI login surfaces only as its own CLI provider; API
provider lights up only when the user sets its own API key.

Changes:
- `model_catalog.rs::detect_auth`: drop `has_cli_fallback` and the
  `read_codex_credential()` clause from `has_key_fallback`. API
  providers now only look at their own (and registered alias) env
  vars. Renamed `has_key_fallback` → `has_key_alias` to reflect
  that it's for documented env-var aliases (GOOGLE_API_KEY for
  Gemini), not CLI-credential borrowing. Removed the unused
  `read_codex_credential()` helper.
- `drivers/mod.rs`: runtime path no longer falls back to the Codex
  auth.json when `OPENAI_API_KEY` is unset — behavior now matches
  detection. Removed `read_codex_credential()` and its callers.
- `providers.rs::test_provider`: removed the dead `ConfiguredCli`
  branch; no API provider produces that status anymore.

`AuthStatus::ConfiguredCli` variant itself is kept (public enum) but
is no longer produced. Existing matches treat it as available, so
any stale serialised value round-trips harmlessly.

Added regression tests in `model_catalog.rs`:
- `detect_auth_does_not_promote_api_providers_from_cli_login`
- `google_api_key_alias_still_recognised_for_gemini`
@github-actions github-actions Bot added the area/runtime Agent loop, LLM drivers, WASM sandbox label Apr 24, 2026
@houko houko changed the title fix(llm-drivers): stop treating CLI config files as auth credentials fix(providers): surface CLI logins as their own providers, not API-provider fallbacks Apr 24, 2026
The two regression tests each declared their own `static ENV_LOCK: Mutex<()>`,
which meant the locks were disjoint and parallel execution could race on
`GEMINI_API_KEY` / `GOOGLE_API_KEY`. Promote the mutex to module scope so
both tests contend on the same lock.
@houko
houko merged commit 34cd436 into main Apr 24, 2026
19 checks passed
@houko
houko deleted the fix/cli-provider-detection branch April 24, 2026 16:01
@github-actions github-actions Bot removed the ready-for-review PR is ready for maintainer review label Apr 24, 2026
houko added a commit that referenced this pull request Apr 24, 2026
…3061)

Follow-up to #3059, which stopped silently promoting CLI logins into
API-provider auth status. That fix surfaced a UX gap: users with only
a CLI login (e.g. Codex CLI signed in, no `OPENAI_API_KEY` set) and
`provider = "auto"` (or a default config pointing at an API provider
they never configured) now hit:

    WARN Primary LLM driver init failed — trying auto-detect
         provider=openai error=Missing API key
    WARN No LLM drivers available — agents will return errors

The daemon's `detect_available_provider()` only scanned API-key env
vars; CLI providers were skipped by the `!p.key_required` guard, so
there was no way for auto-detect to recover.

Changes:
- `drivers/mod.rs::detect_available_provider`: add Phase 3 that scans
  CLI-backed providers (`claude-code`, `codex-cli`, `gemini-cli`,
  `qwen-code`) after all API-key providers. Returns an empty
  `api_key_env` to signal "no env lookup needed — driver handles its
  own auth". API-key providers still win when both are available so
  users with an API key get the direct-API path.
- `kernel/mod.rs` (two call sites) and `cli/main.rs`: render an empty
  env_var as "CLI login" in log messages / user-facing strings so
  operators can tell at a glance that the daemon chose a CLI
  subprocess. Avoids trailing "()" / "from  —" artefacts.

With this change, a machine with only `~/.codex/auth.json` boots with
`provider = "codex-cli"` instead of falling into the "no drivers
available" state.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/runtime Agent loop, LLM drivers, WASM sandbox size/L 250-999 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant