diff --git a/packages/ui/src/components/sections/providers/ProvidersPage.test.ts b/packages/ui/src/components/sections/providers/ProvidersPage.test.ts index 4445ca78..5bb461f3 100644 --- a/packages/ui/src/components/sections/providers/ProvidersPage.test.ts +++ b/packages/ui/src/components/sections/providers/ProvidersPage.test.ts @@ -89,6 +89,15 @@ describe('provider credential state helpers', () => { expect(providerHasCredentials({ key: undefined, authSourceExists: true })).toBe(true); }); + test('providerHasCredentials treats options.apiKey as a usable credential', () => { + // Config-defined providers ship provider.options to the client but never + // reach Provider.key, so the only authoritative signal is options.apiKey. + expect(providerHasCredentials({ key: undefined, authSourceExists: false, optionsApiKey: 'sk-config' })).toBe(true); + expect(providerHasCredentials({ key: undefined, authSourceExists: false, optionsApiKey: '' })).toBe(false); + expect(providerHasCredentials({ key: undefined, authSourceExists: false, optionsApiKey: ' ' })).toBe(false); + expect(providerHasCredentials({ key: undefined, authSourceExists: false, optionsApiKey: null })).toBe(false); + }); + test('env-less OAuth-only provider without credentials opens panel and hides models', () => { const hasCredentials = providerHasCredentials({ key: undefined, diff --git a/packages/ui/src/components/sections/providers/ProvidersPage.tsx b/packages/ui/src/components/sections/providers/ProvidersPage.tsx index e6f35a0c..18269918 100644 --- a/packages/ui/src/components/sections/providers/ProvidersPage.tsx +++ b/packages/ui/src/components/sections/providers/ProvidersPage.tsx @@ -860,6 +860,7 @@ export const ProvidersPage: React.FC = () => { const hasCredentials = providerHasCredentials({ key: selectedProvider.key, authSourceExists: selectedSources?.auth.exists, + optionsApiKey: (selectedProvider as { options?: { apiKey?: string | null } }).options?.apiKey ?? null, }); const authStatusIncomplete = sourcesLoaded && !hasCredentials; const showModelsSection = shouldShowModelsSection({ diff --git a/packages/ui/src/components/sections/providers/providerAuth.ts b/packages/ui/src/components/sections/providers/providerAuth.ts index f57d744e..7cc7b5ac 100644 --- a/packages/ui/src/components/sections/providers/providerAuth.ts +++ b/packages/ui/src/components/sections/providers/providerAuth.ts @@ -66,6 +66,14 @@ export interface ProviderCredentialInput { key?: string | null; /** OpenChamber auth.json provenance for this provider. */ authSourceExists?: boolean | null; + /** + * Provider.options is shipped to the client for config-defined providers + * but never reaches `Provider.key` (upstream only sets `key` from a single + * resolved env var or an api-type auth.json entry). Treat a non-empty + * `options.apiKey` as a usable login, per + * `packages/web/server/lib/walkthrough/DOCUMENTATION.md:134`. + */ + optionsApiKey?: string | null; } /** @@ -76,6 +84,9 @@ export const providerHasCredentials = (input: ProviderCredentialInput): boolean if (typeof input.key === 'string' && input.key.trim().length > 0) { return true; } + if (typeof input.optionsApiKey === 'string' && input.optionsApiKey.trim().length > 0) { + return true; + } return input.authSourceExists === true; };