diff --git a/packages/ui/src/components/sections/providers/ProvidersPage.test.ts b/packages/ui/src/components/sections/providers/ProvidersPage.test.ts index 1861568c..25d4dbcc 100644 --- a/packages/ui/src/components/sections/providers/ProvidersPage.test.ts +++ b/packages/ui/src/components/sections/providers/ProvidersPage.test.ts @@ -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. diff --git a/packages/ui/src/components/sections/providers/ProvidersPage.tsx b/packages/ui/src/components/sections/providers/ProvidersPage.tsx index 7dc52592..32a819ce 100644 --- a/packages/ui/src/components/sections/providers/ProvidersPage.tsx +++ b/packages/ui/src/components/sections/providers/ProvidersPage.tsx @@ -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({ diff --git a/packages/ui/src/components/sections/providers/providerAuth.ts b/packages/ui/src/components/sections/providers/providerAuth.ts index cff0ca5b..61a570f4 100644 --- a/packages/ui/src/components/sections/providers/providerAuth.ts +++ b/packages/ui/src/components/sections/providers/providerAuth.ts @@ -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; };