Skip to content

fix(providers): stop "provider not found" for env-derived profiles#716

Merged
kevincodex1 merged 3 commits into
Gitlawb:mainfrom
PierrunoYT:fix/provider-not-found-707
Jul 20, 2026
Merged

fix(providers): stop "provider not found" for env-derived profiles#716
kevincodex1 merged 3 commits into
Gitlawb:mainfrom
PierrunoYT:fix/provider-not-found-707

Conversation

@PierrunoYT

@PierrunoYT PierrunoYT commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Fixes #707

Problem

RemoveProvider/SetActiveProvider/SetProviderModel only ever look at rows persisted in config.json. But a provider can appear in the resolved provider list (TUI picker, zero providers list) purely because Resolve() synthesized it in-memory from an ambient env var (e.g. OPENAI_API_KEY) without ever writing it to disk.

That mismatch caused a confusing provider "openai" not found error when a user tried to:

  • Delete the provider from the TUI Providers manager (press d)
  • Select a model for it (/model picker) — showed not saved (provider "openai" not found)
  • Run zero providers use <name> or zero providers remove <name> from the CLI

Fix

Added config.ProviderPersisted(path, name) to distinguish "not on disk" from "doesn't exist anywhere," and used it at all four call sites so an env-derived provider gets an accurate, non-error message instead of a bogus not-found failure:

  • internal/tui/provider_manager.go — provider manager delete
  • internal/tui/command_center.go/model picker persistence
  • internal/cli/provider_onboarding.gozero providers use / zero providers remove
  • internal/config/writer.go — the new shared ProviderPersisted helper

Genuinely unknown/misspelled provider names still fall through to the original "not found" error.

Testing

  • go build ./...
  • go vet ./...
  • go test ./internal/cli/... ./internal/config/... ./internal/tui/...

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling for environment-derived providers that aren’t saved in configuration, with clearer behavior for use, remove, and rename (including no-op messaging instead of incorrect saved-config operations).
    • Prevented model save and provider edit actions from proceeding for session-only providers without a saved profile.
  • Improvements

    • Standardized --json output for env-derived use/remove/rename, including consistent persisted and configPath reporting.
    • Added CLI tests for malformed config handling and env-derived provider flows to prevent regressions.

Copilot AI review requested due to automatic review settings July 17, 2026 12:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a mismatch between the resolved provider list (which can include env-derived providers synthesized at runtime) and the persisted provider list in config.json, which previously caused confusing provider "openai" not found errors when users tried to remove/use/update models for env-derived providers (issue #707).

Changes:

  • Adds config.ProviderPersisted(path, name) to distinguish providers that exist on disk vs. providers that only exist in-memory due to environment variables.
  • Updates TUI provider deletion and model persistence paths to avoid treating env-derived providers as “not found” when there’s nothing to mutate on disk.
  • Updates CLI zero providers use/remove to provide clearer handling for unpersisted-but-resolved provider names.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
internal/tui/provider_manager.go Avoids “not found” on deleting env-derived providers by removing them from the session list with an explanatory message.
internal/tui/command_center.go Skips persisting model selection for providers that aren’t persisted in config.json.
internal/config/writer.go Introduces ProviderPersisted helper to detect whether a provider exists as a config row.
internal/cli/provider_onboarding.go Handles providers use/remove for unpersisted providers by resolving config and emitting a non-error explanation when appropriate.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/config/writer.go Outdated
Comment on lines +189 to +205
func ProviderPersisted(path string, name string) bool {
path = strings.TrimSpace(path)
name = strings.TrimSpace(name)
if path == "" || name == "" {
return false
}
cfg, err := loadConfigFile(path)
if err != nil {
return false
}
for _, provider := range cfg.Providers {
if strings.EqualFold(strings.TrimSpace(provider.Name), name) {
return true
}
}
return false
}
Comment on lines +506 to +509
message := fmt.Sprintf(
"Provider %q is not saved in config.json (likely set via an environment variable), so there is no saved profile to switch to.\nIt is already used automatically whenever its environment variable is set; unset it to stop Zero from detecting it automatically.",
options.name,
)
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cb5898dc-201b-47d3-9bf5-4eb6303063ed

📥 Commits

Reviewing files that changed from the base of the PR and between c932d59 and 807c826.

📒 Files selected for processing (2)
  • internal/cli/provider_onboarding.go
  • internal/cli/provider_onboarding_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/cli/provider_onboarding.go

Walkthrough

Provider operations now distinguish persisted providers from environment-derived providers. CLI commands report unpersisted providers, while TUI model persistence, editing, and deletion avoid invalid config writes and update session state appropriately.

Changes

Provider persistence handling

Layer / File(s) Summary
Persisted provider detection
internal/config/writer.go
Adds ProviderPersisted, which performs trimmed, case-insensitive lookups in the on-disk provider configuration and propagates config-load errors.
CLI reporting for unpersisted providers
internal/cli/provider_onboarding.go, internal/cli/provider_onboarding_test.go
providers use, providers remove, and providers rename resolve environment-derived providers, emit text or JSON status, preserve unknown-provider handling, and test malformed-config and env-derived flows.
TUI persistence and deletion behavior
internal/tui/command_center.go, internal/tui/provider_manager.go
Model persistence and editing skip providers absent from config.json; deletion removes persisted entries from disk while handling session-only providers in memory with explanatory notes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI as Provider CLI
  participant Config as ProviderPersisted
  participant Center as Command center resolution
  CLI->>Config: Check provider persistence
  Config-->>CLI: Return persisted status
  CLI->>Center: Resolve unpersisted provider
  Center-->>CLI: Return provider match
  CLI-->>CLI: Emit text or JSON status
Loading

Possibly related PRs

  • Gitlawb/zero#161: Both changes modify provider onboarding flow in internal/cli/provider_onboarding.go.
  • Gitlawb/zero#560: Both changes modify provider removal and rename handling in internal/cli/provider_onboarding.go.
  • Gitlawb/zero#725: Both changes modify providers use behavior for persisted and runtime-derived providers.

Suggested reviewers: anandh8x, vasanthdev2004, kevincodex1

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main fix: suppressing false "provider not found" errors for env-derived profiles.
Linked Issues check ✅ Passed The PR addresses #707 by distinguishing persisted from env-derived providers in delete, selection, and CLI flows.
Out of Scope Changes check ✅ Passed The changes stay focused on provider persistence checks, UX messaging, and related tests, with no clear unrelated scope.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/config/writer.go`:
- Around line 195-197: Update the config-loading flow around loadConfigFile to
return (bool, error) instead of collapsing load failures into false. Propagate
missing, unreadable, and malformed configuration errors to callers before the
unpersisted path, and update each caller to handle the returned error without
treating the operation as an env-derived provider.

In `@internal/tui/provider_manager.go`:
- Around line 361-371: The unpersisted provider branch in
providerManagerCleanupCmd must remain session-only and must not delete
credential-store data. Restrict the same-named credential cleanup to the
persisted-removal path, while preserving the existing informational notes and
session-list removal for env-derived providers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 39727400-8876-4ca6-9194-694f471df6d9

📥 Commits

Reviewing files that changed from the base of the PR and between 727ad4d and 26aec5d.

📒 Files selected for processing (4)
  • internal/cli/provider_onboarding.go
  • internal/config/writer.go
  • internal/tui/command_center.go
  • internal/tui/provider_manager.go

Comment thread internal/config/writer.go Outdated
Comment thread internal/tui/provider_manager.go

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The core fix is right ProviderPersisted cleanly separates "not on disk" from "doesn't exist anywhere," the four call sites you guarded are correct, the /model picker still applies the model in-session for env-derived providers, and the typo fall-through still hits the real "not found". But the class fix is incomplete two more reachable name-keyed mutators have the identical env-derived gap, and the providers remove --json shape has a type inconsistency.

  1. zero providers rename (provider_onboarding.go:455) calls config.RenameProvider with no ProviderPersisted guard, so rename openai myopenai on an env-derived openai still fails provider "openai" not found the same bug you closed for use/remove. It needs the same persisted-check + explanatory message (or fall-through) as the siblings.

  2. The TUI /providers edit-save path (saveManagerEdit, provider_manager.go:611) calls config.EditProvider with no guard. You guarded the sibling delete path (deleteManagerSelection) in this same file, but pressing e on an env-derived row, editing a field, and saving hits provider "openai" not found the same way. Env-derived rows do reach the editor — savedProviders is seeded from usableSavedProviders(resolved.Providers), so the env-derived profile is in the manager list and editable. Needs the same ProviderPersisted branch as delete.

  3. providers remove --json emits removed as a STRING on success (line 389, the name) but a BOOLEAN on the env-derived path (line 547, false) same key, different JSON type, both exit 0. A typed consumer breaks on the env-derived path; the repo's own test struct at provider_onboarding_test.go:263 unmarshals removed into a string, so that pattern would fail to parse {removed: false}. Pick one type e.g. keep removed as the name-string (empty when nothing removed) and convey "not persisted" via the existing message/persisted fields.

Non-blocking: the env-derived use/remove --json payloads also omit configPath/keyRemoved that the success payload always includes worth aligning the field set or documenting the env-derived shape as a distinct variant.

Requesting changes for 1–3.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/cli/provider_onboarding.go (1)

514-536: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Include configPath in the JSON payload.

The success path for providers use --json includes configPath. The unpersisted path should include it as well to maintain a predictable JSON schema. Update the function signature to accept configPath and add it to the JSON response.

🐛 Proposed fix
-func reportUnpersistedProviderUse(stdout, stderr io.Writer, deps appDeps, options providerUseOptions) (int, bool) {
+func reportUnpersistedProviderUse(stdout, stderr io.Writer, deps appDeps, options providerUseOptions, configPath string) (int, bool) {
 	resolved, exitCode := resolveCommandCenterConfig(stderr, deps)
 	if exitCode != exitSuccess {
 		// resolveCommandCenterConfig already wrote its own error to stderr;
 		// stop here instead of letting the caller try SetActiveProvider too.
 		return exitCode, true
 	}
 	if !providerResolvedByName(resolved.Providers, options.name) {
 		return exitCode, false
 	}
 	message := fmt.Sprintf(
 		"Provider %q is not saved in config.json (likely set via an environment variable), so there is no saved profile to switch to.\nIt is available whenever its environment variable is set, but is only active when selected (for example via ZERO_PROVIDER); unset its environment variable to stop Zero from detecting it automatically.",
 		options.name,
 	)
 	if options.json {
 		if err := writePrettyJSON(stdout, map[string]any{
 			"activeProvider": resolved.ActiveProvider,
 			"persisted":      false,
+			"configPath":     configPath,
 			"message":        message,
 		}); err != nil {

Then, update the call site in runProvidersUse:

-		if exit, handled := reportUnpersistedProviderUse(stdout, stderr, deps, options); handled {
+		if exit, handled := reportUnpersistedProviderUse(stdout, stderr, deps, options, configPath); handled {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/provider_onboarding.go` around lines 514 - 536, Update
reportUnpersistedProviderUse to accept configPath, update its call site in
runProvidersUse to pass configPath, and include configPath in the providers use
--json payload alongside activeProvider, persisted, and message.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/cli/provider_onboarding.go`:
- Around line 593-596: Update the success-path response built by writePrettyJSON
so the renamed field uses nil instead of false, preserving the expected
object-or-null JSON type while leaving the other fields unchanged.

---

Outside diff comments:
In `@internal/cli/provider_onboarding.go`:
- Around line 514-536: Update reportUnpersistedProviderUse to accept configPath,
update its call site in runProvidersUse to pass configPath, and include
configPath in the providers use --json payload alongside activeProvider,
persisted, and message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 517764e2-8f5e-4fcc-bcdc-5b5e6738d5f0

📥 Commits

Reviewing files that changed from the base of the PR and between 26aec5d and c932d59.

📒 Files selected for processing (5)
  • internal/cli/provider_onboarding.go
  • internal/cli/provider_onboarding_test.go
  • internal/config/writer.go
  • internal/tui/command_center.go
  • internal/tui/provider_manager.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/tui/command_center.go

Comment thread internal/cli/provider_onboarding.go
@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

fix coderabbit changes @PierrunoYT

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really close now — all three of my asks landed. The rename guard with the typo fall-through, the edit-save guard in the provider manager, and the remove --json payload keeping removed a string plus picking up configPath/keyRemoved all look right, and the schema test on the remove payload is a nice touch. The malformed-config propagation and keeping the credential cleanup on the persisted-delete path only are also correct.

One thing left, and it's the same trap we just closed on remove: the new env-derived rename path emits renamed: false while the success path emits renamed as a {from, to} object (provider_onboarding.go:593). Same key, two JSON types, both exit 0 — a typed consumer parses one and chokes on the other. Emit null (or an empty object) there and I'm good to approve. The use payload omitting configPath is still take-it-or-leave-it from my side.

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-verified on the current head (807c826), and all three of my earlier asks landed cleanly:

  • The rename path now has the ProviderPersisted guard with the same typo fall-through as use/remove, so a genuinely unknown name still hits the real "not found".
  • The TUI edit-save path (saveManagerEdit) is guarded too, rejecting an env-derived row with a clear "no saved profile to edit" message instead of the bogus not-found.
  • The --json schema is consistent now: removed is a string on both paths (name on success, "" on the env path) and renamed is an object on success and null (not false) on the env path, so a typed consumer parses both. The env-derived use payload also picks up configPath.

I walked every call site of the five name-keyed config mutators (RemoveProvider, SetActiveProvider, SetProviderModel, RenameProvider, EditProvider) to be sure the class fix is actually complete: all six reachable ones are guarded, the credential-store cleanup still runs only on the persisted delete path, and the persisted paths are unchanged. Build, vet, and the config/cli/tui tests are green, including the four new env-derived onboarding tests.

One optional, non-blocking note for later (nothing to do here): switchProviderModel in command_center.go makes two name-keyed persist calls that are safe today only because they discard the error with _, _ =, unlike persistSelectedModel which you guarded explicitly. It behaves correctly for env-derived providers (the persist just no-ops), so it is not a bug, but if a future change ever starts checking those errors the same not-found would resurface. Worth a guard for symmetry someday, not in this PR.

Nice work seeing this through the rounds. Approving.

@kevincodex1 kevincodex1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@kevincodex1
kevincodex1 merged commit 4cbd144 into Gitlawb:main Jul 20, 2026
7 checks passed
Vasanthdev2004 added a commit that referenced this pull request Jul 21, 2026
…#767)

Closes #721.

`zero providers use <name>` wrote activeProvider to config.json and reported
"Active provider set to <name>" even when ZERO_PROVIDER was set, which applyEnv
makes win over config.json unconditionally. So the switch was reported as a
success while `providers current` still showed the env-pinned provider, a silent
no-op.

Detect the override and say so: the text path prints a note to stderr naming the
env var and the provider that stays active, and the --json path adds
effectiveProvider / overriddenByEnv fields so a machine consumer is not misled
either. The config write still lands unchanged; only the reporting is honest.

The env read is injected via a new appDeps.getenv (os.Getenv in production, left
nil by fillAppDeps like exportActiveProvider) so tests stay hermetic against an
ambient ZERO_PROVIDER. The related env-derived-profile "not found" case the issue
also mentions is already handled by the unpersisted-provider path (#707) and #716.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Provider deletion leaves stale OpenAI provider, causing "provider not found" and preventing Codex authentication

4 participants