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.<id>.
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.
This commit is contained in:
herjarsa
2026-08-28 16:59:52 +02:00
parent addcbfe447
commit 75d93f79c0
3 changed files with 21 additions and 0 deletions
@@ -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,
@@ -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({
@@ -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;
};