diff --git a/desktop/app_test.go b/desktop/app_test.go
index 8f7d8e1bc..cc7a87993 100644
--- a/desktop/app_test.go
+++ b/desktop/app_test.go
@@ -574,6 +574,9 @@ func TestSettingsLoadsActiveWorkspaceCredentialsWithUserConfig(t *testing.T) {
if !p.KeySet {
t.Fatalf("workspace provider keySet = false, want true from active workspace .env: %+v", p)
}
+ if !p.Configured {
+ t.Fatalf("workspace provider configured = false, want true from active workspace .env: %+v", p)
+ }
return
}
}
diff --git a/desktop/frontend/src/__tests__/provider-model-refresh.test.ts b/desktop/frontend/src/__tests__/provider-model-refresh.test.ts
index 9dfb2a4a8..0f2b87960 100644
--- a/desktop/frontend/src/__tests__/provider-model-refresh.test.ts
+++ b/desktop/frontend/src/__tests__/provider-model-refresh.test.ts
@@ -7,7 +7,9 @@ import {
mergedFetchedProviderModels,
providerApiKeyEnvForSave,
providerDefaultModel,
+ providerIsConfigured,
providerModelCandidates,
+ providerRequiresKey,
} from "../lib/providerModels";
let passed = 0;
@@ -137,5 +139,17 @@ eq(
"preserves an explicitly configured key env",
);
+eq(
+ [
+ providerRequiresKey({ apiKeyEnv: "" }),
+ providerIsConfigured({ apiKeyEnv: "", keySet: false }),
+ providerIsConfigured({ apiKeyEnv: "LOCAL_API_KEY", keySet: false, requiresKey: false }),
+ providerIsConfigured({ apiKeyEnv: "REMOTE_API_KEY", keySet: false, requiresKey: true }),
+ providerIsConfigured({ apiKeyEnv: "REMOTE_API_KEY", keySet: true, requiresKey: true }),
+ ],
+ [false, true, true, false, true],
+ "separates provider selectability from key presence for no-auth providers",
+);
+
console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`);
if (failed > 0) process.exit(1);
diff --git a/desktop/frontend/src/components/SettingsPanel.tsx b/desktop/frontend/src/components/SettingsPanel.tsx
index 9cf285cd9..2ee225d31 100644
--- a/desktop/frontend/src/components/SettingsPanel.tsx
+++ b/desktop/frontend/src/components/SettingsPanel.tsx
@@ -5,7 +5,7 @@ import { asArray } from "../lib/array";
import { useDeferredClose } from "../lib/useMountTransition";
import { app } from "../lib/bridge";
import { normalizeLangPref, useI18n, useT, type DictKey, type LangPref } from "../lib/i18n";
-import { apiKeyEnvFromProviderName, inferredVisionModels, mergedFetchedProviderModels, providerApiKeyEnvForSave, providerDefaultModel, providerModelCandidates } from "../lib/providerModels";
+import { apiKeyEnvFromProviderName, inferredVisionModels, mergedFetchedProviderModels, providerApiKeyEnvForSave, providerDefaultModel, providerIsConfigured, providerModelCandidates, providerRequiresKey } from "../lib/providerModels";
import { useUpdater } from "../lib/useUpdater";
import {
THEME_STYLES,
@@ -569,7 +569,7 @@ function ShortcutsSection() {
function allRefs(s: SettingsView): string[] {
const out: string[] = [];
for (const p of s.providers) {
- if (!p.added || !p.keySet) continue;
+ if (!p.added || !providerIsConfigured(p)) continue;
for (const m of p.models) out.push(`${p.name}/${m}`);
}
return out;
@@ -754,6 +754,7 @@ function normalizeBotMappingScope(scope: unknown, workspaceRoot: unknown): "glob
function normalizeProviderView(p: ProviderView): ProviderView {
const visionModels = asArray(p.visionModels);
+ const requiresKey = providerRequiresKey(p);
return {
...p,
builtIn: Boolean(p.builtIn),
@@ -764,6 +765,8 @@ function normalizeProviderView(p: ProviderView): ProviderView {
modelsUrl: p.modelsUrl ?? "",
reasoningProtocol: normalizeReasoningProtocol(p.reasoningProtocol),
supportedEfforts: asArray(p.supportedEfforts),
+ requiresKey,
+ configured: providerIsConfigured({ ...p, requiresKey }),
keySource: p.keySource ?? "",
keySourcePath: p.keySourcePath ?? "",
};
@@ -2966,7 +2969,7 @@ function ModelsSection({ s, busy, apply, backgroundApply }: ModelsSectionProps)
const defaultProviderView = s.providers.find((p) => p.name === defaultProvider);
const modelIssue = !defaultProviderView
? t("settings.modelUnavailable", { ref: defaultRef || t("common.none") })
- : !defaultProviderView.keySet
+ : !providerIsConfigured(defaultProviderView)
? t("settings.modelNeedsKey", { provider: modelProviderLabel(defaultProvider, defaultProviderView, t) })
: "";
const agent = s.agent ?? { temperature: 0, maxSteps: 0, plannerMaxSteps: 12, systemPrompt: "", coldResumePrune: true, reasoningLanguage: "auto" };
@@ -2979,11 +2982,11 @@ function ModelsSection({ s, busy, apply, backgroundApply }: ModelsSectionProps)
const groups = providerAccessGroups(s.providers.filter((p) => p.added), t);
const candidates = groups
.map((group) => {
- const provider = group.providers.find((p) => p.keySet && p.apiKeyEnv && p.baseUrl);
+ const provider = group.providers.find((p) => providerIsConfigured(p) && p.baseUrl);
return provider ? { group, provider } : null;
})
.filter((item): item is { group: ProviderAccessGroup; provider: ProviderView } => Boolean(item));
- const refreshKey = candidates.map(({ group, provider }) => `${group.id}:${provider.apiKeyEnv}`).join("|");
+ const refreshKey = candidates.map(({ group, provider }) => `${group.id}:${provider.apiKeyEnv || provider.name}:${provider.baseUrl}`).join("|");
if (!refreshKey || autoRefreshKeyRef.current === refreshKey) return;
autoRefreshKeyRef.current = refreshKey;
@@ -3211,6 +3214,7 @@ function ModelPicker({
groupID,
label: firstProvider ? providerGroupLabel(firstProvider, t) : groupID,
keySet: providerViews.some((p) => p.keySet),
+ requiresKey: providerViews.every((p) => providerRequiresKey(p)),
options: uniqueModelOptions(options.filter((opt) => modelOptionGroupID(opt) === groupID)),
};
})
@@ -3279,7 +3283,7 @@ function ModelPicker({
{group.label}
- {group.keySet ? t("settings.keySet") : t("settings.noKey")}
+ {providerKeyStatusLabel(group, t)}
{group.options.map((opt) => (
{group.description}
@@ -3853,7 +3864,7 @@ function ProviderAccessCard({
)}