fix(providers): keep declared env vars as a credential signal

Providers that read credentials from several environment variables
(Bedrock, Azure, Vertex) never get a single resolved Provider.key from
OpenCode, so dropping the env signal made them show "Credentials
missing", auto-open the auth panel, and hide their models even when
fully configured.

providerHasCredentials takes an envDeclared input again, and both
call sites in ProvidersPage pass whether the provider declares any
non-empty env var name.
This commit is contained in:
Bohdan Triapitsyn
2026-08-29 00:50:49 +03:00
parent e13f27c1a3
commit c788cd2237
3 changed files with 33 additions and 4 deletions
@@ -77,8 +77,8 @@ describe('provider auth method helpers', () => {
});
describe('provider credential state helpers', () => {
test('providerHasCredentials ignores declared env names and requires key or auth source', () => {
// Built-in catalog entry with env var names but no actual credential.
test('providerHasCredentials requires key, options.apiKey, declared env, or auth source', () => {
// Built-in catalog entry with no credential signal at all.
expect(providerHasCredentials({ key: undefined, authSourceExists: false })).toBe(false);
expect(providerHasCredentials({ key: '', authSourceExists: false })).toBe(false);
expect(providerHasCredentials({ key: ' ', authSourceExists: false })).toBe(false);
@@ -89,6 +89,14 @@ describe('provider credential state helpers', () => {
expect(providerHasCredentials({ key: undefined, authSourceExists: true })).toBe(true);
});
test('providerHasCredentials counts declared env vars for multi-variable providers', () => {
// Bedrock/Azure/Vertex resolve credentials from several env vars, so
// OpenCode never sets Provider.key for them; the declared env list is the
// only signal that the provider is configured.
expect(providerHasCredentials({ key: undefined, authSourceExists: false, envDeclared: true })).toBe(true);
expect(providerHasCredentials({ key: undefined, authSourceExists: false, envDeclared: false })).toBe(false);
});
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.
@@ -53,6 +53,14 @@ import {
type ProviderConfigScope,
} from './custom-provider-form';
/**
* Providers whose credentials come from several env vars (Bedrock, Azure,
* Vertex) never get a single resolved `Provider.key` from OpenCode, so the
* declared env list is the only signal that they are configured at all.
*/
const providerDeclaresEnv = (provider: { env?: string[] } | undefined): boolean =>
Array.isArray(provider?.env) && provider.env.some((name) => name.trim().length > 0);
const formatCompactNumber = (value: number) => new Intl.NumberFormat(getCurrentIntlLocale(), {
notation: 'compact',
compactDisplay: 'short',
@@ -340,6 +348,7 @@ export const ProvidersPage: React.FC = () => {
key: provider?.key,
authSourceExists: sources.auth.exists,
optionsApiKey: (provider as { options?: { apiKey?: string | null } } | undefined)?.options?.apiKey ?? null,
envDeclared: providerDeclaresEnv(provider),
});
const isEditableCustomProvider = Boolean(
provider && isConfigDefinedCustomProvider(provider, sources)
@@ -860,6 +869,7 @@ export const ProvidersPage: React.FC = () => {
key: selectedProvider.key,
authSourceExists: selectedSources?.auth.exists,
optionsApiKey: (selectedProvider as { options?: { apiKey?: string | null } }).options?.apiKey ?? null,
envDeclared: providerDeclaresEnv(selectedProvider),
});
const authStatusIncomplete = requiresProviderAuth(sourcesLoaded, hasCredentials, isEditableCustomProvider);
const showModelsSection = shouldShowModelsSection({
@@ -74,11 +74,19 @@ export interface ProviderCredentialInput {
* `packages/web/server/lib/walkthrough/DOCUMENTATION.md:134`.
*/
optionsApiKey?: string | null;
/**
* The provider declares environment variables it reads credentials from.
* Multi-variable providers (Bedrock, Azure, Vertex) never resolve a single
* `Provider.key` upstream, so without this signal they read as
* "Credentials missing" even when fully configured.
*/
envDeclared?: boolean;
}
/**
* Prefer authoritative credential signals. Do not treat Provider.env length as
* proof of credentials — that array is declared env var *names*, not values.
* Prefer authoritative credential signals. Declared env vars are the weakest of
* them — the array holds variable *names*, not values — but for providers whose
* credentials span several env vars it is the only signal OpenCode exposes.
*/
export const providerHasCredentials = (input: ProviderCredentialInput): boolean => {
if (typeof input.key === 'string' && input.key.trim().length > 0) {
@@ -87,6 +95,9 @@ export const providerHasCredentials = (input: ProviderCredentialInput): boolean
if (typeof input.optionsApiKey === 'string' && input.optionsApiKey.trim().length > 0) {
return true;
}
if (input.envDeclared === true) {
return true;
}
return input.authSourceExists === true;
};