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({ )}