Skip to content

Commit 089c7a9

Browse files
committed
fix: align no-auth provider settings state
1 parent 5dc86b4 commit 089c7a9

13 files changed

Lines changed: 155 additions & 14 deletions

File tree

desktop/app_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,9 @@ func TestSettingsLoadsActiveWorkspaceCredentialsWithUserConfig(t *testing.T) {
574574
if !p.KeySet {
575575
t.Fatalf("workspace provider keySet = false, want true from active workspace .env: %+v", p)
576576
}
577+
if !p.Configured {
578+
t.Fatalf("workspace provider configured = false, want true from active workspace .env: %+v", p)
579+
}
577580
return
578581
}
579582
}

desktop/frontend/src/__tests__/provider-model-refresh.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import {
77
mergedFetchedProviderModels,
88
providerApiKeyEnvForSave,
99
providerDefaultModel,
10+
providerIsConfigured,
1011
providerModelCandidates,
12+
providerRequiresKey,
1113
} from "../lib/providerModels";
1214

1315
let passed = 0;
@@ -137,5 +139,17 @@ eq(
137139
"preserves an explicitly configured key env",
138140
);
139141

142+
eq(
143+
[
144+
providerRequiresKey({ apiKeyEnv: "" }),
145+
providerIsConfigured({ apiKeyEnv: "", keySet: false }),
146+
providerIsConfigured({ apiKeyEnv: "LOCAL_API_KEY", keySet: false, requiresKey: false }),
147+
providerIsConfigured({ apiKeyEnv: "REMOTE_API_KEY", keySet: false, requiresKey: true }),
148+
providerIsConfigured({ apiKeyEnv: "REMOTE_API_KEY", keySet: true, requiresKey: true }),
149+
],
150+
[false, true, true, false, true],
151+
"separates provider selectability from key presence for no-auth providers",
152+
);
153+
140154
console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`);
141155
if (failed > 0) process.exit(1);

desktop/frontend/src/components/SettingsPanel.tsx

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { asArray } from "../lib/array";
55
import { useDeferredClose } from "../lib/useMountTransition";
66
import { app } from "../lib/bridge";
77
import { normalizeLangPref, useI18n, useT, type DictKey, type LangPref } from "../lib/i18n";
8-
import { apiKeyEnvFromProviderName, inferredVisionModels, mergedFetchedProviderModels, providerApiKeyEnvForSave, providerDefaultModel, providerModelCandidates } from "../lib/providerModels";
8+
import { apiKeyEnvFromProviderName, inferredVisionModels, mergedFetchedProviderModels, providerApiKeyEnvForSave, providerDefaultModel, providerIsConfigured, providerModelCandidates, providerRequiresKey } from "../lib/providerModels";
99
import { useUpdater } from "../lib/useUpdater";
1010
import {
1111
THEME_STYLES,
@@ -569,7 +569,7 @@ function ShortcutsSection() {
569569
function allRefs(s: SettingsView): string[] {
570570
const out: string[] = [];
571571
for (const p of s.providers) {
572-
if (!p.added || !p.keySet) continue;
572+
if (!p.added || !providerIsConfigured(p)) continue;
573573
for (const m of p.models) out.push(`${p.name}/${m}`);
574574
}
575575
return out;
@@ -754,6 +754,7 @@ function normalizeBotMappingScope(scope: unknown, workspaceRoot: unknown): "glob
754754

755755
function normalizeProviderView(p: ProviderView): ProviderView {
756756
const visionModels = asArray(p.visionModels);
757+
const requiresKey = providerRequiresKey(p);
757758
return {
758759
...p,
759760
builtIn: Boolean(p.builtIn),
@@ -764,6 +765,8 @@ function normalizeProviderView(p: ProviderView): ProviderView {
764765
modelsUrl: p.modelsUrl ?? "",
765766
reasoningProtocol: normalizeReasoningProtocol(p.reasoningProtocol),
766767
supportedEfforts: asArray(p.supportedEfforts),
768+
requiresKey,
769+
configured: providerIsConfigured({ ...p, requiresKey }),
767770
keySource: p.keySource ?? "",
768771
keySourcePath: p.keySourcePath ?? "",
769772
};
@@ -2966,7 +2969,7 @@ function ModelsSection({ s, busy, apply, backgroundApply }: ModelsSectionProps)
29662969
const defaultProviderView = s.providers.find((p) => p.name === defaultProvider);
29672970
const modelIssue = !defaultProviderView
29682971
? t("settings.modelUnavailable", { ref: defaultRef || t("common.none") })
2969-
: !defaultProviderView.keySet
2972+
: !providerIsConfigured(defaultProviderView)
29702973
? t("settings.modelNeedsKey", { provider: modelProviderLabel(defaultProvider, defaultProviderView, t) })
29712974
: "";
29722975
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)
29792982
const groups = providerAccessGroups(s.providers.filter((p) => p.added), t);
29802983
const candidates = groups
29812984
.map((group) => {
2982-
const provider = group.providers.find((p) => p.keySet && p.apiKeyEnv && p.baseUrl);
2985+
const provider = group.providers.find((p) => providerIsConfigured(p) && p.baseUrl);
29832986
return provider ? { group, provider } : null;
29842987
})
29852988
.filter((item): item is { group: ProviderAccessGroup; provider: ProviderView } => Boolean(item));
2986-
const refreshKey = candidates.map(({ group, provider }) => `${group.id}:${provider.apiKeyEnv}`).join("|");
2989+
const refreshKey = candidates.map(({ group, provider }) => `${group.id}:${provider.apiKeyEnv || provider.name}:${provider.baseUrl}`).join("|");
29872990
if (!refreshKey || autoRefreshKeyRef.current === refreshKey) return;
29882991
autoRefreshKeyRef.current = refreshKey;
29892992

@@ -3211,6 +3214,7 @@ function ModelPicker({
32113214
groupID,
32123215
label: firstProvider ? providerGroupLabel(firstProvider, t) : groupID,
32133216
keySet: providerViews.some((p) => p.keySet),
3217+
requiresKey: providerViews.every((p) => providerRequiresKey(p)),
32143218
options: uniqueModelOptions(options.filter((opt) => modelOptionGroupID(opt) === groupID)),
32153219
};
32163220
})
@@ -3279,7 +3283,7 @@ function ModelPicker({
32793283
<div className="settings-model-picker__group" key={group.groupID}>
32803284
<div className="settings-model-picker__group-title">
32813285
<span>{group.label}</span>
3282-
<small>{group.keySet ? t("settings.keySet") : t("settings.noKey")}</small>
3286+
<small>{providerKeyStatusLabel(group, t)}</small>
32833287
</div>
32843288
{group.options.map((opt) => (
32853289
<button
@@ -3319,10 +3323,15 @@ function modelOptionFromRef(ref: string, s: SettingsView): ModelPickerOption | n
33193323
}
33203324

33213325
function modelOptionMeta(option: ModelPickerOption, t: ReturnType<typeof useT>): string {
3322-
const key = option.providerView?.keySet ? t("settings.keySet") : t("settings.noKey");
3326+
const key = option.providerView ? providerKeyStatusLabel(option.providerView, t) : t("settings.noKey");
33233327
return `${modelProviderLabel(option.provider, option.providerView, t)} · ${key}`;
33243328
}
33253329

3330+
function providerKeyStatusLabel(provider: { keySet: boolean; requiresKey?: boolean; apiKeyEnv?: string }, t: ReturnType<typeof useT>): string {
3331+
if (!providerRequiresKey(provider)) return t("settings.noKeyRequired");
3332+
return provider.keySet ? t("settings.keySet") : t("settings.noKey");
3333+
}
3334+
33263335
function modelProviderLabel(provider: string, providerView: ProviderView | undefined, t: ReturnType<typeof useT>): string {
33273336
return providerView ? providerGroupLabel(providerView, t) : provider;
33283337
}
@@ -3623,6 +3632,8 @@ type ProviderAccessGroup = {
36233632
providers: ProviderView[];
36243633
apiKeyEnv: string;
36253634
keySet: boolean;
3635+
requiresKey: boolean;
3636+
configured: boolean;
36263637
keySource?: string;
36273638
keySourcePath?: string;
36283639
baseUrl: string;
@@ -3835,7 +3846,7 @@ function ProviderAccessCard({
38353846
{group.builtIn ? t("settings.builtinProviderBadge") : t("settings.customProviderBadge")}
38363847
</span>
38373848
<span className={`badge ${group.keySet ? "badge--project" : "badge--feedback"}`}>
3838-
{group.keySet ? t("settings.keySet") : t("settings.noKey")}
3849+
{providerKeyStatusLabel(group, t)}
38393850
</span>
38403851
</div>
38413852
<div className="provider-access-card__desc">{group.description}</div>
@@ -3853,7 +3864,7 @@ function ProviderAccessCard({
38533864
)}
38543865
<button
38553866
className="btn btn--small"
3856-
disabled={busy || fetching || !group.baseUrl || !group.apiKeyEnv || !group.keySet}
3867+
disabled={busy || fetching || !group.baseUrl || !group.configured}
38573868
onClick={onRefresh}
38583869
>
38593870
{fetching ? t("settings.fetchingModels") : t("settings.fetchModels")}
@@ -3885,8 +3896,8 @@ function ProviderAccessCard({
38853896
</div>
38863897

38873898
<div className="provider-card-block">
3888-
<div className="provider-card-block__label">{t(group.keySet ? "settings.enabledModels" : "settings.modelList")}</div>
3889-
<div className="provider-model-chips" aria-label={t(group.keySet ? "settings.enabledModels" : "settings.modelList")}>
3899+
<div className="provider-card-block__label">{t(group.configured ? "settings.enabledModels" : "settings.modelList")}</div>
3900+
<div className="provider-model-chips" aria-label={t(group.configured ? "settings.enabledModels" : "settings.modelList")}>
38903901
{visibleModels.length > 0 ? visibleModels.map((model) => (
38913902
<span className="provider-model-chip" key={model}>
38923903
{model}
@@ -3898,7 +3909,7 @@ function ProviderAccessCard({
38983909
</span>
38993910
)}
39003911
</div>
3901-
{!group.keySet && (
3912+
{!group.configured && group.requiresKey && (
39023913
<div className="provider-card-status provider-card-status--warn">
39033914
{t("settings.modelsRequireKey")}
39043915
</div>
@@ -4065,6 +4076,8 @@ function providerAccessGroups(providers: ProviderView[], t: ReturnType<typeof us
40654076
if (existing) {
40664077
existing.providers.push(p);
40674078
existing.keySet = existing.keySet || p.keySet;
4079+
existing.requiresKey = existing.requiresKey && providerRequiresKey(p);
4080+
existing.configured = existing.configured || providerIsConfigured(p);
40684081
if (!existing.keySource && p.keySource) existing.keySource = p.keySource;
40694082
if (!existing.keySourcePath && p.keySourcePath) existing.keySourcePath = p.keySourcePath;
40704083
existing.models = uniqueStrings([...existing.models, ...p.models]);
@@ -4078,6 +4091,8 @@ function providerAccessGroups(providers: ProviderView[], t: ReturnType<typeof us
40784091
providers: [p],
40794092
apiKeyEnv: p.apiKeyEnv,
40804093
keySet: p.keySet,
4094+
requiresKey: providerRequiresKey(p),
4095+
configured: providerIsConfigured(p),
40814096
keySource: p.keySource,
40824097
keySourcePath: p.keySourcePath,
40834098
baseUrl: p.baseUrl,

desktop/frontend/src/lib/bridge.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type * as GeneratedApp from "../../wailsjs/go/main/App";
99

1010
import { addBreadcrumb } from "./breadcrumbs";
1111
import { t } from "./i18n";
12+
import { providerRequiresKey } from "./providerModels";
1213
import { DEFAULT_STATUS_BAR_ITEMS, normalizeStatusBarItems } from "./statusBarItems";
1314
import { modeWithAutoApproveTools, modeWithPlan, normalizeCollaborationMode, normalizeMode, normalizeTokenMode, normalizeToolApprovalMode } from "./types";
1415

@@ -2394,7 +2395,7 @@ function makeMockApp(): AppBindings {
23942395
},
23952396
async FetchProviderModels(p: ProviderView) {
23962397
if (!p.baseUrl.trim()) throw new Error(t("settings.fetchModelsMissingBaseUrl"));
2397-
if (!p.apiKeyEnv.trim()) throw new Error(t("settings.fetchModelsMissingKeyEnv"));
2398+
if (providerRequiresKey(p) && !p.apiKeyEnv.trim()) throw new Error(t("settings.fetchModelsMissingKeyEnv"));
23982399
await delay(350);
23992400
if (p.baseUrl.includes("deepseek")) return ["deepseek-v4-flash", "deepseek-v4-pro"];
24002401
if (p.baseUrl.includes("mimo") || p.baseUrl.includes("xiaomimimo")) return ["mimo-v2.5", "mimo-v2.5-pro"];

desktop/frontend/src/lib/providerModels.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@ export function providerDefaultModel(currentDefault: string, models: string[]):
1616
return currentDefault && models.includes(currentDefault) ? currentDefault : models[0] ?? "";
1717
}
1818

19+
export function providerRequiresKey(provider: { requiresKey?: boolean; apiKeyEnv?: string }): boolean {
20+
if (typeof provider.requiresKey === "boolean") return provider.requiresKey;
21+
return Boolean((provider.apiKeyEnv ?? "").trim());
22+
}
23+
24+
export function providerIsConfigured(provider: { configured?: boolean; requiresKey?: boolean; apiKeyEnv?: string; keySet?: boolean }): boolean {
25+
if (typeof provider.configured === "boolean") return provider.configured;
26+
return !providerRequiresKey(provider) || Boolean(provider.keySet);
27+
}
28+
1929
export function providerApiKeyEnvForSave(name: string, apiKeyEnv: string, keyDraft: string): string {
2030
const explicit = apiKeyEnv.trim();
2131
if (explicit) return explicit;

desktop/frontend/src/lib/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,8 @@ export interface ProviderView {
657657
default: string;
658658
apiKeyEnv: string;
659659
keySet: boolean; // the env var currently resolves to a value
660+
requiresKey?: boolean; // false for explicit no-auth providers
661+
configured?: boolean; // selectable: key is set or no key is required
660662
keySource?: string;
661663
keySourcePath?: string;
662664
balanceUrl: string; // optional wallet-balance endpoint; "" disables the readout

desktop/frontend/src/locales/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,7 @@ export const en = {
828828
"settings.notificationSoundPreview": "Preview",
829829
"settings.keySet": "key set",
830830
"settings.noKey": "no key",
831+
"settings.noKeyRequired": "no key required",
831832
"settings.cantDeleteDefault": "Can't delete the default provider",
832833
"settings.cantRemoveDefault": "Can't remove access for the default provider",
833834
"settings.deleteProvider": "Delete provider",

desktop/frontend/src/locales/zh-TW.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,7 @@ export const zhTW: Record<DictKey, string> = {
570570
"settings.stepLimit.custom": "自訂",
571571
"settings.keySet": "已設金鑰",
572572
"settings.noKey": "無金鑰",
573+
"settings.noKeyRequired": "無需金鑰",
573574
"settings.cantDeleteDefault": "無法刪除預設模型服務",
574575
"settings.cantRemoveDefault": "無法移除預設模型服務接入",
575576
"settings.deleteProvider": "刪除模型服務",

desktop/frontend/src/locales/zh.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,7 @@ export const zh: Record<DictKey, string> = {
830830
"settings.notificationSoundPreview": "预览",
831831
"settings.keySet": "已设密钥",
832832
"settings.noKey": "无密钥",
833+
"settings.noKeyRequired": "无需密钥",
833834
"settings.cantDeleteDefault": "无法删除默认模型服务",
834835
"settings.cantRemoveDefault": "无法移除默认模型服务接入",
835836
"settings.deleteProvider": "删除模型服务",

desktop/settings_app.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ type ProviderView struct {
3838
Default string `json:"default"`
3939
APIKeyEnv string `json:"apiKeyEnv"`
4040
KeySet bool `json:"keySet"` // the env var currently resolves to a non-empty value
41+
RequiresKey bool `json:"requiresKey"`
42+
Configured bool `json:"configured"` // selectable: either key is present or no key is required
4143
KeySource string `json:"keySource,omitempty"`
4244
KeySourcePath string `json:"keySourcePath,omitempty"`
4345
BalanceURL string `json:"balanceUrl"`
@@ -285,11 +287,14 @@ func providerViewFromEntryForRoot(p config.ProviderEntry, builtIn, added bool, r
285287
visionModels = models
286288
}
287289
key := config.ResolveCredentialForRootGlobalFirst(root, p.APIKeyEnv)
290+
requiresKey := p.RequiresAPIKey()
288291
return ProviderView{
289292
Name: p.Name, BuiltIn: builtIn, Added: added, Kind: p.Kind, BaseURL: p.BaseURL,
290293
Models: nonNil(models), VisionModels: nonNil(providerVisionModels(models, visionModels)), VisionModelsSet: visionModelsSet, ModelsURL: p.ModelsURL, Default: p.DefaultModel(),
291294
APIKeyEnv: p.APIKeyEnv,
292295
KeySet: key.Set,
296+
RequiresKey: requiresKey,
297+
Configured: !requiresKey || key.Set,
293298
KeySource: key.Source.Label,
294299
KeySourcePath: key.Source.Path,
295300
BalanceURL: p.BalanceURL,

0 commit comments

Comments
 (0)