From 75d93f79c074fa93e13de7998207c86832763712 Mon Sep 17 00:00:00 2001 From: herjarsa Date: Fri, 28 Aug 2026 16:59:52 +0200 Subject: [PATCH] fix(providers): treat options.apiKey as a credential in credential signal The previous review flagged that providerHasCredentials misclassifies working config-defined providers: Provider.key is only set by upstream when exactly one declared env var resolves or an api-type auth.json entry exists. Config providers only get options, so provider.. options.apiKey never reaches key. Result: a provider whose key is embedded in opencode.json showed 'Credentials missing', lost the Models section, and forced the auth panel open. Add optionsApiKey to ProviderCredentialInput and check it in providerHasCredentials alongside key and authSourceExists. This matches what main's requiresProviderAuth helper used to do and honors the OpenChamber docs contract that options.apiKey counts as a usable login (walkthrough/DOCUMENTATION.md:134). Wire the new field through ProvidersPage.tsx using a typed indirection: the SDK Provider type does not yet expose options publicly, so the read site casts the object to the known shape. This keeps the call site type-safe without waiting for an SDK update. --- .../sections/providers/ProvidersPage.test.ts | 9 +++++++++ .../components/sections/providers/ProvidersPage.tsx | 1 + .../src/components/sections/providers/providerAuth.ts | 11 +++++++++++ 3 files changed, 21 insertions(+) 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; };