fix(providers): wire optionsApiKey through both call sites; restore editable custom exemption

The previous fix added optionsApiKey to providerHasCredentials but
wired it through only one of the two call sites in ProvidersPage.tsx.
The auto-open effect at line 339 still omitted it, so a config-defined
provider whose only credential is options.apiKey would get the auth
panel force-opened on every selection while the summary beside it
said Connected; the dismissal resets on provider switch, so the
panel re-opens each time.

Restore the isEditableCustomProvider exemption that main's
requiresProviderAuth helper carried into both authStatusIncomplete
and shouldShowModelsSection. A keyless local custom provider (LM
Studio / Ollama style) regressed from 'models visible, no banner' to
'Credentials missing' with the models section hidden.

Rewire requiresProviderAuth (its only production consumer was lost
in the rebase) by delegating authStatusIncomplete to it. The helper
already encodes sourcesLoaded && !hasCredentials && !isEditableCustom
Provider, so the call site is one line and the contract matches main.

shouldShowModelsSection now accepts an optional isEditableCustom
Provider flag that lifts the credential gate for editable providers,
matching the exemption the ProvidersPage.test.ts:22 fixture asserts.

Tests: 15/15 pass. Was 14 in the previous commit; added one test
for the editable custom exemption.
This commit is contained in:
herjarsa
2026-08-28 17:47:34 +02:00
parent 75d93f79c0
commit 65668f1453
3 changed files with 40 additions and 3 deletions
@@ -133,6 +133,31 @@ describe('provider credential state helpers', () => {
})).toBe(true); })).toBe(true);
}); });
test('editable custom provider keeps models visible even with no credentials signal', () => {
// Config-defined custom providers (e.g. local LM Studio/Ollama style)
// are user-editable in place; a stale 'Credentials missing' must not
// hide their models section. Without the exemption, a keyless local
// custom provider regresses to 'Credentials missing' with models hidden.
const hasCredentials = providerHasCredentials({
key: undefined,
authSourceExists: false,
optionsApiKey: null,
});
expect(hasCredentials).toBe(false);
expect(shouldShowModelsSection({
modelCount: 1,
sourcesLoaded: true,
hasCredentials: false,
isEditableCustomProvider: true,
})).toBe(true);
expect(shouldShowModelsSection({
modelCount: 1,
sourcesLoaded: true,
hasCredentials: false,
isEditableCustomProvider: false,
})).toBe(false);
});
test('auth save followed by providers refresh recognizes credentials without stale missing state', () => { test('auth save followed by providers refresh recognizes credentials without stale missing state', () => {
// Pre-save: sources say no auth, provider has no key yet. // Pre-save: sources say no auth, provider has no key yet.
const before = providerHasCredentials({ const before = providerHasCredentials({
@@ -27,7 +27,7 @@ import type { ModelMetadata } from '@/types';
import { getCurrentIntlLocale, useI18n } from '@/lib/i18n'; import { getCurrentIntlLocale, useI18n } from '@/lib/i18n';
import { runtimeFetch } from '@/lib/runtime-fetch'; import { runtimeFetch } from '@/lib/runtime-fetch';
import { opencodeClient } from '@/lib/opencode/client'; import { opencodeClient } from '@/lib/opencode/client';
import { shouldLoadAvailableProviders } from './providerAvailability'; import { requiresProviderAuth, shouldLoadAvailableProviders } from './providerAvailability';
import { import {
getOAuthAuthMethods, getOAuthAuthMethods,
parseAuthPayload, parseAuthPayload,
@@ -339,6 +339,7 @@ export const ProvidersPage: React.FC = () => {
const hasCreds = providerHasCredentials({ const hasCreds = providerHasCredentials({
key: provider?.key, key: provider?.key,
authSourceExists: sources.auth.exists, authSourceExists: sources.auth.exists,
optionsApiKey: (provider as { options?: { apiKey?: string | null } } | undefined)?.options?.apiKey ?? null,
}); });
const isEditableCustomProvider = Boolean( const isEditableCustomProvider = Boolean(
provider && isConfigDefinedCustomProvider(provider, sources) provider && isConfigDefinedCustomProvider(provider, sources)
@@ -862,11 +863,12 @@ export const ProvidersPage: React.FC = () => {
authSourceExists: selectedSources?.auth.exists, authSourceExists: selectedSources?.auth.exists,
optionsApiKey: (selectedProvider as { options?: { apiKey?: string | null } }).options?.apiKey ?? null, optionsApiKey: (selectedProvider as { options?: { apiKey?: string | null } }).options?.apiKey ?? null,
}); });
const authStatusIncomplete = sourcesLoaded && !hasCredentials; const authStatusIncomplete = requiresProviderAuth(sourcesLoaded, hasCredentials, isEditableCustomProvider);
const showModelsSection = shouldShowModelsSection({ const showModelsSection = shouldShowModelsSection({
modelCount: providerModels.length, modelCount: providerModels.length,
sourcesLoaded, sourcesLoaded,
hasCredentials, hasCredentials,
isEditableCustomProvider,
}); });
const incompleteAuthHint = !showApiKeyAuth && oauthAuthMethods.length > 0 const incompleteAuthHint = !showApiKeyAuth && oauthAuthMethods.length > 0
? t('settings.providers.page.auth.useReconnectHint') ? t('settings.providers.page.auth.useReconnectHint')
@@ -94,7 +94,17 @@ export const shouldShowModelsSection = (input: {
modelCount: number; modelCount: number;
sourcesLoaded: boolean; sourcesLoaded: boolean;
hasCredentials: boolean; hasCredentials: boolean;
}): boolean => input.modelCount > 0 && (!input.sourcesLoaded || input.hasCredentials); /**
* Config-defined custom providers (providerSources.custom present and parsed
* via `isConfigDefinedCustomProvider`) are user-editable in place, so a
* stale `Credentials missing` signal must not hide their models section.
* Optional for back-compat; defaults to `false`, restoring the pre-rewrite
* exemption that `requiresProviderAuth` carried via `providerAvailability.ts`.
*/
isEditableCustomProvider?: boolean;
}): boolean =>
input.modelCount > 0 &&
(!input.sourcesLoaded || input.hasCredentials || Boolean(input.isEditableCustomProvider));
export const shouldAutoOpenAuthPanel = (input: { export const shouldAutoOpenAuthPanel = (input: {
sourcesLoaded: boolean; sourcesLoaded: boolean;