Skip to content

feat(cli): /model with no argument opens a cross-provider model picker - #9658

Closed
jlmalone wants to merge 24 commits into
aaif-goose:mainfrom
jlmalone:feat/model-cross-provider-picker
Closed

feat(cli): /model with no argument opens a cross-provider model picker#9658
jlmalone wants to merge 24 commits into
aaif-goose:mainfrom
jlmalone:feat/model-cross-provider-picker

Conversation

@jlmalone

@jlmalone jlmalone commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

What

A bare /model in a goose session currently switches models within the active provider only. This makes a no-argument /model open one searchable menu that aggregates models from every configured provider and switches the live session's provider and model on selection. /model <name> keeps its existing within-provider behavior.

Why

With several providers configured (e.g. a local OpenAI-compatible server plus a hosted provider), switching to a model on a different provider mid-session means leaving the session or editing config. This lets you jump to any model on any configured provider in one fuzzy-search step.

How

  • pick_and_switch_model() enumerates configured providers, fetches each provider's models, aggregates them, reuses the existing configure-flow fuzzy-search UI, then create() + update_provider() on the session. interactive_model_search is now pub(crate) for reuse.
  • Only offers providers you can actually use. provider_is_configured() requires a provider that authenticates with a secret to have a real secret value, not just a built-in default — so default-only cloud providers (e.g. Bedrock, whose AWS_REGION has a default) don't appear as usable. NoAuth local backends (LM Studio, Ollama) declare no secrets and stay available.
  • Quiet about what's unavailable. Providers that can't be listed here (uninstalled ACP commands, unreachable local servers) collapse into a single Skipped N provider(s) not available here line instead of a wall of per-provider warnings. Listing is bounded by a 15s per-provider timeout so one slow endpoint can't block the menu.
  • Respects context-managed providers. When the current provider keeps authoritative state in a subprocess (ACP / Claude Code / Gemini CLI), bare /model is rejected up front — same guard /model <name> already applied — so the picker can't switch away and strand that context.
  • Probes before committing. The chosen model gets a 30s-bounded check before the switch — providers can list catalog models the account can't actually run (e.g. NVIDIA returns 404 "function not found"), so the picker reports that and keeps the current model rather than silently switching to a dead one. Context-managed targets are skipped here: they have no dead-catalog-model failure mode, and probing them could consume their one-time handoff context or hang on the ACP cancellation path.

Testing

  • cargo fmt, cargo check -p goose-cli, and cargo clippy -p goose-cli -- -D warnings clean against current main (branch merged up to date).
  • Regression test for the provider-gating predicate (Bedrock-like default-only, local no-auth, and credentialed cases).
  • Manually: bare /model lists models across configured providers and switches the session; unreachable endpoints are skipped rather than blocking the list; a model the account can't run is rejected with the current model kept; /model <name> unchanged.

Two follow-ups from review (NoAuth-OpenAI gating and Claude Code subprocess cleanup, both provider-side) are tracked in #9822. Every commit is DCO signed-off.

Bare /model now lists models from every configured provider (local LM Studio, NVIDIA, etc.) in one searchable menu and switches the live session's provider and model on selection. /model <name> keeps its existing within-provider behavior.

pick_and_switch_model enumerates configured providers, fetches each provider's models (15s timeout, skip on error), aggregates into one list, reuses the configure fuzzy-search UI, then create()+update_provider. provider_is_configured gates the menu to providers whose required keys resolve via env/secret/default. interactive_model_search is now pub(crate) for reuse.

Signed-off-by: Joseph Malone <jlmalone@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2ccfac992e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/goose-cli/src/session/mod.rs Outdated

for (meta, _ptype) in &all {
let include =
meta.name.as_str() == current_provider_name || provider_is_configured(meta);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Don't offer default-only providers as configured

In a normal install with no AWS credentials, this predicate still treats Amazon Bedrock as configured because its required AWS_REGION key has a built-in default in crates/goose/src/providers/bedrock.rs, and Bedrock's fetch_supported_models returns a static list without validating credentials. That makes bare /model show Bedrock models and allows switching the live session to an unusable provider, with the failure deferred until the next prompt; the picker should require an actual provider configuration/credential signal rather than counting defaults alone.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 3a28adf. A built-in default is now treated as configuration, not a credential: a provider that authenticates with a secret (API key / bearer token) must have at least one real secret value to be offered in /model — so Bedrock no longer appears merely because AWS_REGION has a default. Providers with no secret keys (LM Studio, Ollama, and other local no-auth backends) declare no secrets and stay always available. Added a regression test covering the Bedrock-like, local, and credentialed cases.

provider_is_configured() counted a required key satisfied by its built-in
default as configured, so the /model picker offered cloud providers with no
usable credentials -- e.g. Bedrock, whose required AWS_REGION has a default.
Treat a built-in default as configuration, not a credential: a provider with
secret keys must have at least one real secret value, while local no-auth
providers (LM Studio, Ollama) declare no secrets and stay available. Adds a
regression test.

Signed-off-by: Joseph Malone <jlmalone@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3a28adf74d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/goose-cli/src/session/mod.rs Outdated
Comment on lines +2330 to +2331
let secret_keys: Vec<_> = meta.config_keys.iter().filter(|k| k.secret).collect();
secret_keys.is_empty() || secret_keys.iter().any(|k| has_value(k))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include API-keyless OpenAI endpoints

When OpenAI is configured only for a local OpenAI-compatible endpoint, e.g. OPENAI_BASE_URL or a non-default OPENAI_HOST with no OPENAI_API_KEY, OpenAiProvider::from_env can use AuthMethod::NoAuth, but this predicate rejects it because OpenAI declares optional secret keys and none have a value. If the current session is on another provider, bare /model will omit that working local OpenAI provider, which is one of the advertised switch targets; consider treating non-secret provider config as sufficient or using the provider's inventory_configured().

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Real gap — and it's in direct tension with the Bedrock fix above. provider_is_configured was tightened to require an actual secret value precisely so default-only cloud providers (Bedrock via its built-in AWS_REGION default) stop appearing as usable; that same strictness now also excludes a NoAuth local OpenAI endpoint (OPENAI_BASE_URL / non-default OPENAI_HOST, no key). Rather than loosen the predicate here and risk re-introducing the Bedrock case, the right fix is to gate on each provider's own configured signal (inventory_configured() / a NoAuth from_env). Tracked in #9822.

Comment thread crates/goose-cli/src/session/mod.rs Outdated
Comment on lines +917 to +920
match tokio::time::timeout(
Duration::from_secs(15),
temp_provider.fetch_supported_models(),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid timing out subprocess model fetches without cleanup

When a configured Claude Code provider is installed but its model_list control request hangs or exceeds 15s, this timeout cancels ClaudeCodeProvider::fetch_supported_models while it is waiting; that function spawned a child from build_stream_json_command() and only calls child.kill().await after exchange_control returns, with no kill_on_drop, so the timed-out /model picker can leave a claude process running. Either make subprocess model listing cancellation-safe or avoid wrapping it in this timeout.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. When the 15s list timeout cancels fetch_supported_models() for a Claude Code provider, the spawned claude child can outlive it because the command isn't kill_on_drop. The picker can't reap a child it doesn't own, so the durable fix belongs in ClaudeCodeProvider (kill_on_drop(true) on the command, or a cancellation-safe control request) rather than wrapping it differently from the picker side. Tracked in #9822.

…noise

The /model picker now avoids landing on models that do not actually work:

- Probes the chosen model with a 30s-bounded request before committing the
  switch. Providers can list catalog models the account cannot run (e.g. NVIDIA
  returns 404 'function not found'); the picker now reports that and keeps the
  current model instead of silently switching to a dead one.
- Collapses the wall of per-provider 'skipping / could not list' warnings
  (uninstalled ACP commands, unreachable local servers) into a single
  'Skipped N provider(s) not available here' line.

Signed-off-by: Joseph Malone <jlmalone@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6be91f44ae

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/goose-cli/src/session/mod.rs Outdated
let probe_msg = [Message::user().with_text("ok")];
match tokio::time::timeout(
Duration::from_secs(30),
new_provider.complete(&probe_config, "model-check", "", &probe_msg, &[]),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid consuming ACP handoff context during probing

When the selected provider is an ACP provider such as claude-acp or codex-acp, this probe runs a real complete on the provider instance that will later be installed. AcpProvider::stream marks its first prompt as the one-time handoff-context opportunity, so the synthetic "ok" prompt consumes that state before update_provider; the user's first real prompt after switching is then treated as a later ACP prompt and the existing conversation history is not sent. Skip this probe for providers that manage their own context, or recreate the provider after probing before installing it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in f7eaa6a. The probe and the to-be-installed provider were the same instance, so the synthetic complete("ok") consumed an ACP provider's one-time handoff-context opportunity before update_provider — the user's first real prompt then dropped the existing history. The picker now probes a throwaway instance, drops it, and creates a fresh provider for the actual switch (ModelConfig/ExtensionConfig are Clone, so the second create() reuses the same config). Verified with cargo check/clippy -p goose-cli.

jlmalone added 2 commits June 16, 2026 03:19
Signed-off-by: Joseph Malone <jlmalone@users.noreply.github.com>
The probe-before-switch ran complete() on the same provider instance that
then gets installed. An ACP provider (claude-acp/codex-acp) treats its first
prompt as the one-time handoff-context opportunity, so the synthetic probe
consumed it and the user's first real prompt after switching dropped the
existing conversation history. Probe a throwaway instance, drop it, and
install a fresh one. Addresses the Codex P2 on the probe path.

Signed-off-by: Joseph Malone <jlmalone@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f7eaa6abf4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 808 to +812
if model.is_none() {
output::goose_mode_message(&format!(
"Current session model: '{}' (provider '{}')",
current_model_name, current_provider_name
));
return Ok(());
return self
.pick_and_switch_model(
&current_provider_name,
&current_model_config,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Guard bare /model for context-managed providers

When the current provider is Claude Code, Gemini CLI, or an ACP provider, this new no-argument path enters the picker before the existing provider.manages_own_context()/-acp checks below run. Those providers keep the authoritative conversation state in the subprocess, which is why /model <name> is rejected; allowing the picker to switch away from them can make the next provider continue from only Goose's partial transcript and lose hidden CLI/ACP context. Apply the same guard before opening the picker for these current providers.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in ab9e2ae. The -acp and manages_own_context() guards now run in handle_model before the model.is_none() dispatch, so bare /model is rejected for a context-managed current provider exactly like /model <name> already was. The picker can no longer switch away from Claude Code / Gemini CLI / an ACP provider and strand the conversation state they hold in their subprocess.

Comment thread crates/goose-cli/src/session/mod.rs Outdated
Comment on lines +1004 to +1007
match tokio::time::timeout(
Duration::from_secs(30),
probe_provider.complete(&probe_config, "model-check", "", &probe_msg, &[]),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make ACP probing cancellation-safe

If the selected provider is an ACP provider and this probe prompt hangs, the 30s timeout cancels the complete future, but dropping the AcpProvider still synchronously joins its client loop while that loop is waiting on the in-flight ACP PromptRequest. In that scenario the command can remain stuck after reporting/attempting the timeout instead of returning to the session; skip probing providers that manage their own context or make the ACP prompt cancellation path shut down the subprocess before joining.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in ab9e2ae by not probing context-managed targets at all. The probe exists only to catch cloud providers that list models the account can't actually run (e.g. NVIDIA 404s); ACP / Claude Code / Gemini CLI don't have that failure mode, and as you note a hung probe would wedge on the client-loop join at drop. The picker now skips the probe when the chosen provider is -acp or manages_own_context() and installs it directly. That also let me drop the throwaway-instance workaround from the previous commit — once we never probe these providers, the handoff-context concern is moot too, so it's a single instance again.

jlmalone added 2 commits June 17, 2026 02:25
Two issues Codex flagged on the probe path, both about providers that keep
authoritative conversation state in a subprocess (ACP, Claude Code, Gemini CLI):

- Bare /model entered the picker before the existing manages_own_context()/-acp
  guard that already rejects /model <name> for these providers, so the picker
  could switch away from them and continue the next provider from only Goose's
  partial transcript. Hoist the guard ahead of the picker dispatch.

- Probing a selected context-managed provider could consume its one-time handoff
  context or, if the probe hung, wedge the cancellation path while the ACP client
  loop is joined on drop. Skip the probe for those providers (they have no
  dead-catalog-model failure mode); probe only the cloud/API providers it was
  meant for. Also removes the throwaway-instance dance the previous commit added.

Signed-off-by: Joseph Malone <jlmalone@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ab9e2ae750

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/goose-cli/src/session/mod.rs Outdated
Comment on lines +897 to +898
let include =
meta.name.as_str() == current_provider_name || provider_is_configured(meta);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include OAuth-token providers in picker

When a user has already signed in to an OAuth-backed provider such as ChatGPT Codex or xAI OAuth and the current session is on a different provider, the usable credential lives in the provider's token cache rather than in Config::global() under the *_TOKEN metadata key; for example, ChatGptCodexProvider::inventory_configured() checks TokenCache::new().load().is_some(). This metadata-only predicate therefore returns false and bare /model omits an actually configured provider from the cross-provider picker, so users cannot switch to it despite being authenticated. Use the registry entry's inventory_configured() or equivalent provider-specific status instead of only ProviderMetadata keys here.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Real gap, but it belongs in the per-provider follow-up (#9822) rather than this metadata predicate. The usable credential for OAuth-backed providers lives in their token cache, which only inventory_configured() (e.g. ChatGptCodexProvider's TokenCache::new().load().is_some()) can see. The shared check_provider_configured works from ProviderMetadata keys and structurally can't observe a provider's token cache. Gating the picker on each registry entry's inventory_configured() is the fix, and it closes this together with the other provider-specific cases.

@DOsinga

DOsinga commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the careful back-and-forth on this @jlmalone — you've landed pretty much every codex point.

Stepping back though: most of the recurring codex comments (default-only Bedrock, API-keyless OpenAI endpoints, and now the OAuth-token providers one from 2026-06-17) are all symptoms of the same thing — provider_is_configured in crates/goose-cli/src/session/mod.rs is a fresh re-implementation of "is this provider configured?", and it's missing cases that the canonical version already handles.

We already have that canonical logic: check_provider_configured in crates/goose-server/src/routes/utils.rs. It correctly handles the local override, HuggingFace OAuth (usable/refreshable tokens), OAuth-flow providers (the structured configured flag / legacy {name}_configured marker), zero-config / !requires_auth providers, and custom/declarative providers loaded from config files — i.e. exactly the OAuth and NoAuth gaps codex keeps flagging and that #9822 tracks.

Rather than keep patching the CLI heuristic, could we share the real one? The CLI can't depend on goose-server, so the move would be to lift check_provider_configured (and its helpers) down into the goose core crate — somewhere under crates/goose/src/providers/ near the inventory/registry — and have both the goose-server routes and this picker call it. That removes the duplication, fixes the OAuth + NoAuth + Bedrock comments in one shot, and lets us close out the deferred items in #9822.

One smaller thing: test_provider_is_configured_default_is_not_a_credential mutates process env via std::env::set_var / remove_var. That's racy under our parallel test runner and we avoid it — once the logic moves to a shared helper that takes config explicitly, the test should be able to assert against that without touching the global environment.

Happy to help wire up the shared helper if useful. Snoozing a few days to give you a chance to weigh in.

jlmalone added 2 commits June 22, 2026 10:37
…ider-picker

# Conflicts:
#	crates/goose-cli/src/commands/configure.rs
The CLI model picker carried its own provider_is_configured heuristic, a
partial re-implementation of "is this provider usable?" that kept missing
cases the rest of goose already handled (default-only Bedrock, API-keyless
OpenAI endpoints, OAuth-token providers). Those were the recurring review
comments and the deferred items in aaif-goose#9822.

Move the canonical check_provider_configured (plus its Hugging Face OAuth
helpers and tests) from goose-server routes::utils down into the goose core
crate (providers::configured), and have both the goose-server config routes
and the CLI picker call it. The picker already held the ProviderType (it was
iterating over (meta, _ptype)), so it now passes that through.

Removes the duplicated heuristic and the racy env-mutating test
(test_provider_is_configured_default_is_not_a_credential); the OAuth, NoAuth,
and Bedrock cases are now handled in one place.

Signed-off-by: Joseph Malone <jlmalone@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 966cb547ce

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


// If there are no non-default keys, this provider needs at least one key explicitly set
if required_non_default_keys.is_empty() {
return required_keys.iter().any(|key| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat optional secrets as configuration signals

When OpenAI is configured only via OPENAI_API_KEY in the environment/keychain and there is no providers.openai.configured entry, this branch returns false because OpenAI's required keys are only the defaulted OPENAI_HOST/OPENAI_BASE_PATH, while the usable optional secret is never considered. Since bare /model now calls this shared predicate before listing cross-provider choices, a working env-keyed OpenAI provider disappears from the picker whenever the current session is on another provider. Fresh evidence after the earlier Bedrock fix is that the final refactor replaced the picker-specific secret check with this required-key-only branch.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 13b18cf. When every required key resolves from a default, check_provider_configured now also treats a real value in any secret key as the configuration signal, so an env/keychain OPENAI_API_KEY (with OPENAI_HOST / OPENAI_BASE_PATH defaulted) marks OpenAI configured and the cross-provider picker lists it. The same secret check now backs the single-required-key branch as well (see the Bedrock thread, 395a8ff).

check_provider_configured only inspected required keys, so a provider whose
required keys all resolve via defaults (e.g. OpenAI, whose HOST and BASE_PATH
are defaulted while the API key is an optional secret) was reported as
unconfigured when set up via its API key alone. The bare /model picker calls
this shared predicate, so such a provider vanished from the cross-provider
list. When every required key resolves from a default, also treat a real value
in any secret key as the configuration signal, keeping the server and CLI
consistent. Fixes the OpenAI case Codex flagged on the refactor.

Signed-off-by: Joseph Malone <jlmalone@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 13b18cfa32

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

let is_set_in_env = env::var(&key.name).is_ok();
let is_set_in_config = config.get(&key.name, key.secret).is_ok();

return is_set_in_env || is_set_in_config;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include optional secrets before rejecting defaulted keys

When Bedrock is configured with AWS_BEARER_TOKEN_BEDROCK and the region comes from the AWS SDK chain such as AWS_DEFAULT_REGION or an AWS profile, this early return ignores the real optional secret and reports the provider as unconfigured unless AWS_REGION itself is explicitly set. Since the bare /model picker filters providers through this predicate before it ever tries to instantiate/list them, a usable Bedrock setup is omitted from cross-provider switching.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 395a8ff. The single-required-key-with-default branch (Bedrock's only required key, AWS_REGION, is defaulted) short-circuited on whether that key was explicitly set and ignored the secret. It now also reports configured when any secret key holds a real value, so a Bedrock setup using AWS_BEARER_TOKEN_BEDROCK with the region resolved from an AWS profile or AWS_DEFAULT_REGION is offered in the picker. I factored the secret check into a shared any_secret_configured helper so this branch and the all-defaulted-required-keys branch can't drift again.

This doesn't reopen the original default-only Bedrock case: a fresh install has neither an explicit AWS_REGION nor a bearer token, so Bedrock stays excluded until a real credential is present.

@jlmalone

Copy link
Copy Markdown
Contributor Author

Thanks Douwe, agreed on consolidating. Done across 966cb547c (refactor) and 13b18cfa3 (a follow-on fix):

  • Lifted the canonical check_provider_configured (plus the HF OAuth helpers and tests) into core at goose::providers::configured, and pointed both the goose-server config routes and the /model picker at it. The picker already had the ProviderType in hand, so it passes it through. Dropped the CLI provider_is_configured heuristic and the racy env-mutating test.
  • Codex then flagged a real gap in the shared predicate: a provider whose required keys all resolve via defaults (e.g. OpenAI, where HOST/BASE_PATH are defaulted and the API key is an optional secret) read as unconfigured when set up via its API key alone, so it dropped out of the picker. Fixed in the shared function: when every required key resolves from a default, a real value in any secret key now counts as configured. Keeps server and CLI consistent and resolves the OAuth/NoAuth/Bedrock family in one place.
  • Also merged main (adapting the moved ModelConfig path); mergeable again, CI green.

I didn't re-add the configured-state test in env-mutating form, since check_provider_configured reads Config::global(). Happy to follow up by having it take config by injection so it's testable without touching global env, if you'd like that in this PR.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fafb9688cf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/goose-cli/src/session/mod.rs Outdated
Comment on lines +923 to +924
let temp_provider =
match goose::providers::create(&meta.name, model_config, Vec::new()).await {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bound provider construction while gathering models

When a configured ACP provider is present but its adapter hangs during initialize/newSession, the picker stalls here before the 15s timeout is installed: goose::providers::create calls the ACP constructor, and AcpProvider::start waits on init_rx and session_rx during construction. That means a single bad configured ACP target can block bare /model indefinitely instead of being counted as skipped; wrap create+fetch in the same timeout or avoid constructing ACP providers just to list models.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in e5eef0e. The 15s timeout wrapped only fetch_supported_models(), so goose::providers::create() ran unbounded and a hung ACP initialize / newSession handshake could stall the picker before the timeout ever applied. create() now runs inside the same timeout as the listing call; a provider that errors, times out, or returns no models is skipped, exactly as before.

A construction cancelled mid-handshake can still leave the adapter's child process around, but that's the provider-side kill_on_drop cleanup tracked in #9822, not something the picker can reap from the outside.

Comment thread crates/goose-cli/src/session/mod.rs Outdated
Comment on lines +1022 to +1024
match tokio::time::timeout(
Duration::from_secs(30),
new_provider.complete(&probe_config, "model-check", "", &probe_msg, &[]),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Don't timeout subprocess-backed probes without cleanup

When the selected target is subprocess-backed but not context-managed, e.g. codex or cursor_agent, a hung CLI can exceed this 30s timeout and cancel complete while those providers' execute_command implementations have spawned a child and only wait after stdout EOF; they do not set kill_on_drop, so the picker can report a timeout while leaving a codex exec/cursor-agent process running. Either make these providers cancellation-safe or skip the probe for them.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, and this is the same provider-side issue as the Claude Code listing leak, just at the probe call site: codex / cursor_agent spawn a child in execute_command without kill_on_drop, so a 30s probe timeout can cancel complete and leave it running. The picker can't reap a child it doesn't own, so the durable fix is kill_on_drop(true) (or a cancellation-safe wait) in those providers rather than a picker-side special case. Folded into #9822 alongside the Claude Code item. The common HTTP providers the probe actually targets (e.g. the NVIDIA dead-catalog 404 case) are unaffected.

jlmalone added 2 commits June 23, 2026 12:30
…d key

check_provider_configured short-circuited the single-required-key-with-default
case (e.g. Bedrock, whose only required key AWS_REGION is defaulted) on whether
that key was explicitly set, ignoring any secret. A Bedrock setup that
authenticates with AWS_BEARER_TOKEN_BEDROCK while the region resolves from an
AWS profile or AWS_DEFAULT_REGION was therefore reported unconfigured, so the
bare /model picker omitted it. Treat a real value in any secret key as the
configuration signal here too, matching the all-defaulted-required-keys branch,
and factor the secret check into a shared helper so the two branches cannot
drift. Fixes the Bedrock case Codex flagged on the refactor.

Signed-off-by: Joseph Malone <jlmalone@users.noreply.github.com>
…timeout

The picker wrapped only fetch_supported_models() in the 15s timeout, leaving
goose::providers::create() unbounded. An ACP provider's create() blocks on the
adapter's initialize/newSession handshake, so a single hung adapter could stall
bare /model indefinitely before the timeout could fire. Move create() inside
the same timeout as the listing call; a provider that errors, times out, or
returns no models is skipped from the menu as before. Fixes the unbounded ACP
construction Codex flagged.

Signed-off-by: Joseph Malone <jlmalone@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e5eef0eb93

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// handoff context or, if it hangs, wedge the cancellation path while the
// client loop is joined on drop — and they don't have the dead-catalog
// failure mode the probe guards against.
if !chosen_provider.ends_with("-acp") && !new_provider.manages_own_context() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject CLI context-managed targets mid-session

When an existing session has history and the user picks a context-managed CLI target such as Gemini CLI or Claude Code from bare /model, this path only skips the probe and then installs the provider. Those providers start with no subprocess context and only forward the latest user message (GeminiCliProvider::build_prompt / ClaudeCodeProvider::last_user_content_blocks), so the prior Goose transcript is silently dropped on the next prompt; either block these targets unless there is no prior history or add an explicit handoff path like ACP uses.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 0c9476567. Bare /model now refuses to switch into a provider that manages its own context (Claude Code, Gemini CLI) when the session already has history, mirroring the current-provider guard in handle_model. Those providers forward only the latest user message and have no ACP-style handoff, so the existing transcript would be silently dropped. ACP targets report manages_own_context() == false and carry their own first-prompt handoff (the reason the probe is skipped for them), so they stay switchable. A real handoff path for the raw CLI providers would be the richer fix but is out of scope for this PR.

@jlmalone

Copy link
Copy Markdown
Contributor Author

@DOsinga thanks, that was the right call. The shared-helper refactor is in: check_provider_configured and its helpers now live in crates/goose/src/providers/configured.rs in the core crate, and both the goose-server routes and the /model picker call it (966cb547c). The duplicate CLI predicate is gone, and so is the racy std::env::set_var test you flagged: it went away with the duplicate, and the remaining configured tests don't touch process env.

With the logic in one place, the recurring config-detection comments collapse to a single helper, and the last Codex nits on it are addressed there:

  • env/keychain-only OpenAI (defaulted OPENAI_HOST / OPENAI_BASE_PATH, API key as the only credential): 13b18cf
  • Bedrock via AWS_BEARER_TOKEN_BEDROCK with the region from an AWS profile or AWS_DEFAULT_REGION: 395a8ff

Both now route through a single any_secret_configured helper, so the two defaulted-required-key branches can't drift apart again. On the picker itself, the unbounded ACP construction is now bounded under the listing timeout (e5eef0e).

The one remaining item is provider-side: a subprocess-backed probe (codex / cursor_agent) can leak a child when the 30s timeout cancels complete. That's a kill_on_drop fix in those providers, folded into #9822 next to the Claude Code listing leak. CI is green. Happy to split any of this into its own PR if you'd prefer.

The bare /model picker (and the configure model search) opened a mandatory
text-input box before showing any models, so a large catalog could not be
browsed without first typing a filter, and there was no way to just arrow to a
model. Use cliclack's native select instead: filter_mode() gives inline
type-to-filter and max_rows() a scrolling viewport, so the list shows up front,
is navigable with the arrow keys, and is selectable without typing. Removes the
bespoke search loop.

Signed-off-by: Joseph Malone <jlmalone@users.noreply.github.com>
Esc / Ctrl-C in the picker surfaced as io::ErrorKind::Interrupted, which the
session loop treated as a fatal error and tore down the whole TUI ("operation
interrupted"). Catch the cancellation (the same signal the elicitation prompts
already handle), keep the current model, and return to the session prompt. The
cross-provider picker now routes through the shared searchable menu for any
provider count, since that menu filters and scrolls on its own.

Signed-off-by: Joseph Malone <jlmalone@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 02ddfe5072

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

};

let (_, chosen_provider, chosen_model) =
match entries.into_iter().find(|(l, _, _)| l == &chosen_label) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use unique values for picker entries

When two rows render the same label, such as a custom/declarative provider using the same display name and model id as another provider, cliclack returns only that label string and this lookup always picks the first matching entry. In that scenario selecting the later row switches to the wrong provider/model; use a stable unique item value (or include the provider name in the value/label) so the selected row maps back to the intended entry.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 0c9476567. cliclack returns only the selected label, so two entries with the same display name and model id mapped back ambiguously and the lookup took the first. The picker now disambiguates any colliding label with the internal provider name before the menu is shown, so each row resolves to exactly one (provider, model). Non-colliding labels are unchanged.

…-safe

Two correctness issues Codex flagged on the cross-provider picker:

- Duplicate entry labels resolved to the wrong provider. cliclack returns
  only the selected label, so two providers sharing a display name and model
  id produced identical rows and the lookup always took the first. Disambiguate
  colliding labels with the internal provider name so each maps to one entry.

- Switching into a context-managed CLI provider (Claude Code, Gemini CLI)
  mid-session silently dropped the transcript: those forward only the latest
  user message and, unlike ACP, have no handoff. Refuse the switch when the
  session already has history, mirroring the existing current-provider guard.

Signed-off-by: Joseph Malone <jlmalone@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0c9476567c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/goose-cli/src/session/mod.rs Outdated
// there is history to lose, mirroring the current-provider guard in
// handle_model. ACP targets report manages_own_context() == false and
// carry their own handoff, so they stay switchable.
if new_provider.manages_own_context() && !self.messages.is_empty() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow ACP targets to use their handoff path

When a non-empty session selects an ACP target such as claude-acp or codex-acp from the bare /model picker, this condition rejects it because AcpProvider::manages_own_context() returns true (crates/goose/src/acp/provider.rs:417), even though ACP has first-prompt handoff logic to carry the current transcript. The picker therefore never installs ACP providers mid-session and instead prints the drop-history error; exclude *-acp targets from this check or use a separate guard for the CLI providers that lack handoff.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, fixed in d975b3e67. AcpProvider::manages_own_context() does return true, so the history guard was rejecting ACP targets even though their first-prompt handoff relays the transcript. The guard now excludes *-acp targets and only blocks the no-handoff CLI providers (Claude Code, Gemini CLI), which is what it was meant to catch.

…lter_mode

filter_mode shares its scroll offset (view.start) with the filtered list and
does not reset it when the list shrinks, so after the list narrows the viewport
could render empty while Enter still selected an off-screen row: typing a query
showed no suggestions yet picked a model blindly. Replace it with a plain
scrollable select (max_rows) plus an explicit Search item that opens a filter
prompt. The list still shows immediately and is arrow-selectable without typing;
filtering is now predictable and always shows what it matched.

Signed-off-by: Joseph Malone <jlmalone@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3d817fc235

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

let key = &required_keys[0];
let is_set_in_env = env::var(&key.name).is_ok();
let is_set_in_config = config.get(&key.name, key.secret).is_ok();
return is_set_in_env || is_set_in_config || any_secret_configured(metadata, config);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include installed default-command providers

When a provider's only required key is a defaulted command, such as CODEX_COMMAND, CURSOR_AGENT_COMMAND, or CLAUDE_CODE_COMMAND, this returns false unless the user explicitly saved that command name. Bare /model filters through this predicate before it ever calls create()/fetch_supported_models(), so an installed and authenticated Codex/Cursor/Claude CLI available under the default executable name is omitted from the cross-provider picker instead of being probed and skipped only if the command is actually unavailable.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same per-provider class as #9822. The metadata predicate can't tell whether a defaulted command (CODEX_COMMAND / CURSOR_AGENT_COMMAND / CLAUDE_CODE_COMMAND) actually resolves to an installed executable on PATH; only attempting the provider (or its own configured check) can. Keying the predicate on the presence of a defaulted command name would mark these configured on every machine. The right behavior, probe and skip when the command is unavailable, needs the per-provider signal in #9822, where I'm folding this in.

…-config API

main decoupled the model config from the provider: create() no longer takes a
ModelConfig, Provider::get_model_config() is gone, and Agent::update_provider()
now takes the model config explicitly. Migrate pick_and_switch_model to match:

- create(name, extensions) instead of create(name, model_config, extensions),
  dropping the now-unused seed/model_config in the listing loop.
- probe with the locally built new_model_config rather than a provider getter.
- update_provider(provider, new_model_config, session_id).

handle_model already picked up main's model_config_for_session() via the merge.
Workspace builds and clippy --all-targets -D warnings is clean.

Signed-off-by: Joseph Malone <jlmalone@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a0d37b53f4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

let key = &required_keys[0];
let is_set_in_env = env::var(&key.name).is_ok();
let is_set_in_config = config.get(&key.name, key.secret).is_ok();
return is_set_in_env || is_set_in_config || any_secret_configured(metadata, config);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include explicit AWS profile Bedrock configs

When Bedrock is usable through the AWS SDK chain, e.g. the user sets AWS_PROFILE and the selected profile supplies the region (or AWS_DEFAULT_REGION) without an explicit AWS_REGION, this branch still reports the provider as unconfigured because it only checks the defaulted required key and secret keys. BedrockProvider::from_env does load AWS_PROFILE and allows the SDK config/profile to resolve the region, but bare /model filters through this predicate before it ever creates the provider, so those working Bedrock setups are omitted from cross-provider switching.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Real, but it's the same blind spot as the other provider-specific cases and shouldn't be patched in this predicate. AWS_PROFILE is a non-required key defaulting to default, and it's commonly set in a developer's shell for unrelated AWS CLI use, so gating Bedrock on it would surface the provider for anyone who has AWS configured at all, reintroducing the fresh-install false positive the strict check was added to prevent. Only BedrockProvider::from_env (which resolves the region from the profile/SDK chain) can tell a Bedrock-capable setup from generic AWS config. Folding into the per-provider inventory_configured() work in #9822.

The context-managed-target guard rejected any provider whose
manages_own_context() is true. AcpProvider returns true there, but ACP
targets carry a first-prompt handoff that relays the existing transcript,
so blocking them was wrong: only the no-handoff CLI providers (Claude Code,
Gemini CLI) actually drop history. Exclude *-acp targets from the guard.

Signed-off-by: Joseph Malone <jlmalone@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d975b3e67d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

{
label.push_str(" (current)");
}
entries.push((label, meta.name.clone(), m));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Don't offer ACP models that cannot be applied

When models comes from an ACP target whose wrapper does not set a model config option, this adds every reported ACP model as a selectable row even though the selected model is never applied. For example, claude-acp sets model_config_option_id: None in crates/goose/src/providers/claude_acp.rs:85, and AcpProvider::apply_model_if_changed returns immediately without an option id in crates/goose/src/acp/provider.rs:334-337; selecting Claude Code ▸ sonnet from bare /model therefore persists sonnet in the Goose session while the ACP subprocess stays on its default/current model. Only expose a sentinel/current row for ACP providers without a model option, or wire the option id before offering per-model choices.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 0bc763e6b. You're right that the per-model rows were misleading for an ACP target that can't apply a model choice: apply_model_if_changed returns early when model_config_option_id is None, so the pick was persisted goose-side while the agent stayed on its own current model.

The picker now asks each provider whether a model pick is meaningful via a new Provider::applies_selected_model() (default true; AcpProvider returns self.model_config_option_id.is_some()). When it's false (Claude Code, Amp, Codex, Pi) the provider gets a single … ▸ (agent's current model) row that switches via the ACP_CURRENT_MODEL sentinel, so nothing is pinned that the agent won't honor. Copilot, which does set model_config_option_id, keeps its full per-model list. Added a unit test for the predicate.

The cross-provider `/model` picker listed every model an ACP target
reported, but ACP wrappers with no model-config option (Claude Code,
Amp, Codex, Pi) ignore a per-model choice: apply_model_if_changed
returns early when model_config_option_id is None, so picking
"Claude Code > sonnet" persisted `sonnet` on the goose side while the
agent silently stayed on its own current model.

Add Provider::applies_selected_model() (default true; AcpProvider
returns model_config_option_id.is_some()). When it is false the picker
offers a single entry that switches to the provider using the
ACP_CURRENT_MODEL sentinel instead of a misleading per-model menu.
Copilot, which does set a model option, keeps its full per-model list.

Surfaced by codex review on aaif-goose#9658.

Signed-off-by: Joseph Malone <jlmalone@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0bc763e6b2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/goose-cli/src/session/mod.rs Outdated
// fire. A provider that errors, times out, or lists nothing is skipped.
let provider_name = meta.name.clone();
let listed = tokio::time::timeout(Duration::from_secs(15), async move {
let temp_provider = goose::providers::create(&provider_name, Vec::new()).await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid applying the active model while listing ACP targets

When the candidate is an ACP provider with a model config option, this construction uses the process-global active model before the picker knows which target model the user wants: CopilotAcpProvider::from_env reads Config::get_goose_model() into its session_config_options (crates/goose/src/providers/copilot_acp.rs:68-73), and AcpProvider applies those options during newSession (crates/goose/src/acp/provider.rs:1185-1204). If the current/global session is on another provider, e.g. OpenAI with GOOSE_MODEL=gpt-4o, a configured Copilot ACP adapter can reject that foreign model before fetch_supported_models() runs, so the picker skips Copilot even though it has valid models to select. Create ACP candidates for listing with the current sentinel or defer applying a model option until after the user selects one.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 78ab68c6b. Real gap: the picker builds each candidate by calling create() then fetch_supported_models(), and Copilot (the only ACP provider with a model config option) folds get_goose_model() into its session_config_options at construction, which the eager newSession then applies. With the session on another provider (e.g. GOOSE_MODEL=gpt-4o under OpenAI), Copilot rejects the foreign model, create() errors, and the provider is dropped even though it lists fine.

The fix takes your first suggestion (list on the current sentinel): a new acp::while_listing_models task-local scope wraps the listing construction, and copilot_acp consults it (model_to_pin) to start model-neutral while listing. Copilot's normal construction is untouched, and its real model is still applied at stream() time via apply_model_if_changed, so runtime behavior is identical. Added unit tests for both the scope and the model_to_pin decision.

The picker lists each provider's models by constructing it and calling
fetch_supported_models(). Copilot, the only ACP provider with a model
config option, pins the active session's model at construction, so when
the session is on another provider (e.g. OpenAI gpt-4o) the Copilot
adapter rejects the foreign model and the provider is dropped from the
picker even though it has valid models to offer.

Add an acp::while_listing_models scope that suppresses ACP model pinning
during listing. Copilot then constructs on its current model and lists
normally; its real model is still applied at stream() time via
apply_model_if_changed, so runtime behavior is unchanged.

Signed-off-by: Joseph Malone <jlmalone@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 78ab68c6b6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/goose-cli/src/session/mod.rs Outdated
Comment on lines +1041 to +1043
let extensions = self.agent.get_extension_configs().await;
let new_provider = goose::providers::create(&chosen_provider, extensions)
.await

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Suppress Copilot model pinning when installing selection

When the user selects copilot-acp while the global GOOSE_MODEL still belongs to another provider, this switch-time create() runs outside the while_listing_models scope, so CopilotAcpProvider::from_env again pins Config::get_goose_model() as a session config option before the chosen model can be applied. The listing path was fixed to avoid this rejection, but the provider that is actually installed is still constructed with the foreign model and can fail the ACP session/setConfigOption handshake, making the visible Copilot choice impossible to switch to.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d9d24938a. Same failure as listing, one call later: the switch-time create() ran outside the suppression scope, so Copilot re-pinned the stale global GOOSE_MODEL and could reject it before the chosen model was ever applied. The scope is renamed without_model_pinning (it no longer only guards listing) and now wraps the install-time create() too; the chosen model rides in new_model_config and is applied by apply_model_if_changed on first use.

Comment thread crates/goose-cli/src/session/mod.rs Outdated
let temp_provider =
goose::providers::create(&provider_name, Vec::new()).await?;
let applies_selected_model = temp_provider.applies_selected_model();
let models = temp_provider.fetch_supported_models().await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Filter picker rows to tool-usable models

This uses the raw provider catalog instead of the existing fetch_recommended_models(global_toolshim()) path used by configuration, so providers whose /models endpoint includes non-text or no-tool-call catalog entries can add unusable rows to the cross-provider picker. The later probe sends only a plain "ok" with no tools, so a text model that cannot do tool calls can pass the probe and then break normal Goose turns with extensions; use the recommended-model filter before adding rows.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d9d24938a. The picker now lists via fetch_recommended_models(global_toolshim()), matching the configure flow and the desktop picker (inventory/mod.rs), so non-text and no-tool-call catalog entries are filtered out the same way everywhere models are offered. Its built-in fallback (filtered-to-empty returns the full catalog) keeps ACP agents, whose model ids often do not map to the canonical registry, listing their full set.

jlmalone added 2 commits July 1, 2026 14:51
main's provider refactor dropped the session_id argument from
Provider::complete. Update the liveness probe to the current 4-argument
signature so the picker still builds after merging the latest main.

Signed-off-by: Joseph Malone <jlmalone@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6536ab79b5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/goose-cli/src/session/mod.rs Outdated
let temp_provider =
goose::providers::create(&provider_name, Vec::new()).await?;
let applies_selected_model = temp_provider.applies_selected_model();
let models = temp_provider.fetch_supported_models().await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use metadata models when provider listing is empty

When a configured provider relies on ProviderMetadata.known_models instead of overriding fetch_supported_models(), this picker treats it as unavailable. For example, with the aws-providers feature, SageMakerTgiProvider lists SAGEMAKER_TGI_DEFAULT_MODEL in metadata but its Provider impl does not override fetch_supported_models(), so the trait default returns an empty vec; the new picker then skips SageMaker and can report no models even though the provider is configured and usable. Fall back to the registry metadata/known models for empty dynamic lists, or add provider-specific list implementations before filtering it out.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d9d24938a. When the filtered listing comes back empty the picker now falls back to the provider metadata known_models, so metadata-only providers like SageMaker TGI (no fetch_supported_models override; the trait default lists nothing) keep their rows instead of being skipped.

The switch-time create() ran outside the pinning-suppression scope, so
an option-backed ACP target (Copilot) re-pinned the stale global model
and could fail the switch before the chosen model was applied; the
scope is renamed to without_model_pinning now that it guards install
as well as listing. The listing now uses fetch_recommended_models, the
same tool-usability filter as configure and the desktop picker, and
falls back to metadata known_models for providers that do not override
fetch_supported_models.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d9d24938a3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +929 to +933
let listed = goose::acp::without_model_pinning(tokio::time::timeout(
Duration::from_secs(15),
async move {
let temp_provider =
goose::providers::create(&provider_name, Vec::new()).await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make ACP listing timeout cancellation-safe

When a configured ACP adapter spawns but never answers initialize/newSession, this timeout cancels goose::providers::create after AcpProvider::start has already spawned the client loop (crates/goose/src/acp/provider.rs:251) but before an AcpProvider exists to run Drop. In that case /model reports the provider as skipped while the detached ACP thread/subprocess can keep running; the fresh evidence after the earlier construction-timeout fix is that the timeout now wraps the constructor itself, whose spawned loop is not owned by any cleanup guard until start returns.

Useful? React with 👍 / 👎.

Comment on lines +974 to +975
Ok(Ok(_)) | Ok(Err(_)) | Err(_) => {
skipped.push(meta.display_name.clone());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Offer current-model ACP targets without model options

When an option-less ACP adapter starts successfully but does not advertise a model config option, AcpProvider::fetch_supported_models returns an error because resolve_model_info requires model config_options, and this arm treats that as an unavailable provider. That omits configured ACP targets that can still run on the agent's current model and should use the sentinel row; fresh evidence after the earlier ACP-row fix is that the final code only adds the sentinel for Ok((false, models)) with a non-empty model list, while the no-model-option path lands here as Ok(Err(_)).

Useful? React with 👍 / 👎.

@github-actions

Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as stale because it has not had recent activity for 23 days.

What happens next?

  • If no further activity occurs, this PR will be automatically closed in 7 days
  • To keep this PR active, simply add a comment, push new commits, or add the keep-open label
  • If you believe this PR was marked as stale in error, please comment and we'll review it

Thank you for your contribution! 🚀

@github-actions github-actions Bot added the stale label Jul 25, 2026
johanndrews added a commit to johanndrews/goose that referenced this pull request Jul 29, 2026
/builtin and /extension add extensions, but nothing showed which ones
are actually loaded — a question that comes up right after a failed
extension start, or after /new restarted them.

/extensions prints a table of the loaded extensions with their type and
description, following the shape /skills already uses.

/provider was considered alongside this and deliberately left out: aaif-goose#9658
turns a bare /model into a cross-provider picker that switches provider
and model together, which covers the same ground more directly.
@filipkujawa

Copy link
Copy Markdown
Collaborator

Closing as superseded- not a reflection of the quality of the work here.

Main has moved past this branch: #10585 rewrote handle_model and landed cross-provider switching (/model --provider [model] with tab completion), goose-server was removed in #10224 (taking the shared check_provider_configured anchor with it), and model enumeration now flows through the provider inventory. A rebase would effectively be a rewrite.

The interactive picker on bare /model is still wanted (#9412), but it's now best done as a fresh, small PR over the inventory on top of the current handle_model.

@jlmalone thanks for the careful iteration. if you'd like to build the successor, anchoring on #9412 first is the way to go.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants