galaxyproject / galaxyproject/loom

Model switch silently falls back to another provider when the selected one has no usable credential — surface it in the UI

Open
#235 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
14
Forks
12
Avg merge
6d 5h
Merged PRs (30d)
17

Description

## Symptom

Switching models appears broken: selecting **Gemini 3.1 Pro** (provider `google`) "flips back" to **gpt-5.5** (provider `openai-codex`). Reported happening repeatedly. From the user's view the switch is accepted, looks successful, then silently reverts — with no explanation.

## Root cause: silent credential fallback

The UI switch works; the **brain** reroutes at startup. When the selected provider has no usable credential, loom falls back to whatever provider *does* have one and only reports it to **stderr** — nothing surfaces in the UI. From the run log:

```
[main] safeStorage unavailable — keys remain plaintext on disk
loom: active provider "google" has no usable credential here; switched to "openai-codex" (signed in via ~/.pi/agent/auth.json).
loom: active provider "deepseek" has no usable credential here; switched to "openai-codex" (signed in via ~/.pi/agent/auth.json).
```

It lands on `openai-codex`/gpt-5.5 specifically because that's the one credential that's readable here: an OAuth token in plaintext `~/.pi/agent/auth.json`. Every API-key provider in the config is stored **encrypted** (`apiKeyEncrypted`), so when safeStorage can't decrypt them, they all become "no usable credential":

| provider | credential | usable in this run |
|---|---|---|
| google, deepseek, anthropic, openai | `apiKeyEncrypted` (safeStorage) | ❌ (safeStorage unavailable) |
| openai-codex | plaintext OAuth in `auth.json` | ✅ |

### Two triggers for "no usable credential"

1. **Dev build (`npm start`)** — safeStorage is unavailable in the electron-forge dev runner, so keys the packaged app saved encrypted can't be decrypted. (This is how it was reproduced; the packaged app would decrypt fine.)
2. **Legitimately missing/invalid key** — a provider with no key, an expired/geo-blocked one, etc. Same silent-fallback behavior.

Either way, the fallback is invisible to the user, which reads as "model switching is broken."

## Proposed fix

Surface the fallback prominently instead of burying it in stderr:

- When the brain switches the active provider because the selected one has no usable credential, emit a clear UI message (chat info card + footer), e.g. *"Gemini has no usable API key here — using gpt-5.5 (openai-codex) instead. Add a Google key in Preferences."*
- Special-case the **dev-mode / safeStorage-unavailable** situation with its own message (*"Encrypted API keys can't be decrypted in this build — re-enter keys or use the packaged app"*), since the key *exists* but is unreadable.
- Reflect the **actual** active provider/model in the footer + model chip after the fallback, so the UI never claims a provider it isn't using.

This is the same "surface provider problems in the UI" theme as #192 (Gemini geo-block messaging) and #195 (recover from silent provider death).

## Acceptance criteria

- [ ] Selecting a provider with no usable credential shows a clear, user-visible explanation (not just stderr).
- [ ] The dev-mode / safeStorage-unavailable case is distinguished from a genuinely missing key.
- [ ] The footer/model chip reflect the provider actually in use after any fallback.

## Related

- #192 — surface Gemini geo-block clearly
- #195 — recover the UI when a provider call dies silently

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.